nucliadb 6.7.2.post4874__py3-none-any.whl → 6.10.0.post5705__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.
Files changed (246) hide show
  1. migrations/0023_backfill_pg_catalog.py +8 -4
  2. migrations/0028_extracted_vectors_reference.py +1 -1
  3. migrations/0029_backfill_field_status.py +3 -4
  4. migrations/0032_remove_old_relations.py +2 -3
  5. migrations/0038_backfill_catalog_field_labels.py +8 -4
  6. migrations/0039_backfill_converation_splits_metadata.py +106 -0
  7. migrations/0040_migrate_search_configurations.py +79 -0
  8. migrations/0041_reindex_conversations.py +137 -0
  9. migrations/pg/0010_shards_index.py +34 -0
  10. nucliadb/search/api/v1/resource/utils.py → migrations/pg/0011_catalog_statistics.py +5 -6
  11. migrations/pg/0012_catalog_statistics_undo.py +26 -0
  12. nucliadb/backups/create.py +2 -15
  13. nucliadb/backups/restore.py +4 -15
  14. nucliadb/backups/tasks.py +4 -1
  15. nucliadb/common/back_pressure/cache.py +2 -3
  16. nucliadb/common/back_pressure/materializer.py +7 -13
  17. nucliadb/common/back_pressure/settings.py +6 -6
  18. nucliadb/common/back_pressure/utils.py +1 -0
  19. nucliadb/common/cache.py +9 -9
  20. nucliadb/common/catalog/__init__.py +79 -0
  21. nucliadb/common/catalog/dummy.py +36 -0
  22. nucliadb/common/catalog/interface.py +85 -0
  23. nucliadb/{search/search/pgcatalog.py → common/catalog/pg.py} +330 -232
  24. nucliadb/common/catalog/utils.py +56 -0
  25. nucliadb/common/cluster/manager.py +8 -23
  26. nucliadb/common/cluster/rebalance.py +484 -112
  27. nucliadb/common/cluster/rollover.py +36 -9
  28. nucliadb/common/cluster/settings.py +4 -9
  29. nucliadb/common/cluster/utils.py +34 -8
  30. nucliadb/common/context/__init__.py +7 -8
  31. nucliadb/common/context/fastapi.py +1 -2
  32. nucliadb/common/datamanagers/__init__.py +2 -4
  33. nucliadb/common/datamanagers/atomic.py +9 -2
  34. nucliadb/common/datamanagers/cluster.py +1 -2
  35. nucliadb/common/datamanagers/fields.py +3 -4
  36. nucliadb/common/datamanagers/kb.py +6 -6
  37. nucliadb/common/datamanagers/labels.py +2 -3
  38. nucliadb/common/datamanagers/resources.py +10 -33
  39. nucliadb/common/datamanagers/rollover.py +5 -7
  40. nucliadb/common/datamanagers/search_configurations.py +1 -2
  41. nucliadb/common/datamanagers/synonyms.py +1 -2
  42. nucliadb/common/datamanagers/utils.py +4 -4
  43. nucliadb/common/datamanagers/vectorsets.py +4 -4
  44. nucliadb/common/external_index_providers/base.py +32 -5
  45. nucliadb/common/external_index_providers/manager.py +5 -34
  46. nucliadb/common/external_index_providers/settings.py +1 -27
  47. nucliadb/common/filter_expression.py +129 -41
  48. nucliadb/common/http_clients/exceptions.py +8 -0
  49. nucliadb/common/http_clients/processing.py +16 -23
  50. nucliadb/common/http_clients/utils.py +3 -0
  51. nucliadb/common/ids.py +82 -58
  52. nucliadb/common/locking.py +1 -2
  53. nucliadb/common/maindb/driver.py +9 -8
  54. nucliadb/common/maindb/local.py +5 -5
  55. nucliadb/common/maindb/pg.py +9 -8
  56. nucliadb/common/nidx.py +22 -5
  57. nucliadb/common/vector_index_config.py +1 -1
  58. nucliadb/export_import/datamanager.py +4 -3
  59. nucliadb/export_import/exporter.py +11 -19
  60. nucliadb/export_import/importer.py +13 -6
  61. nucliadb/export_import/tasks.py +2 -0
  62. nucliadb/export_import/utils.py +6 -18
  63. nucliadb/health.py +2 -2
  64. nucliadb/ingest/app.py +8 -8
  65. nucliadb/ingest/consumer/consumer.py +8 -10
  66. nucliadb/ingest/consumer/pull.py +10 -8
  67. nucliadb/ingest/consumer/service.py +5 -30
  68. nucliadb/ingest/consumer/shard_creator.py +16 -5
  69. nucliadb/ingest/consumer/utils.py +1 -1
  70. nucliadb/ingest/fields/base.py +37 -49
  71. nucliadb/ingest/fields/conversation.py +55 -9
  72. nucliadb/ingest/fields/exceptions.py +1 -2
  73. nucliadb/ingest/fields/file.py +22 -8
  74. nucliadb/ingest/fields/link.py +7 -7
  75. nucliadb/ingest/fields/text.py +2 -3
  76. nucliadb/ingest/orm/brain_v2.py +89 -57
  77. nucliadb/ingest/orm/broker_message.py +2 -4
  78. nucliadb/ingest/orm/entities.py +10 -209
  79. nucliadb/ingest/orm/index_message.py +128 -113
  80. nucliadb/ingest/orm/knowledgebox.py +91 -59
  81. nucliadb/ingest/orm/processor/auditing.py +1 -3
  82. nucliadb/ingest/orm/processor/data_augmentation.py +1 -2
  83. nucliadb/ingest/orm/processor/processor.py +98 -153
  84. nucliadb/ingest/orm/processor/sequence_manager.py +1 -2
  85. nucliadb/ingest/orm/resource.py +82 -71
  86. nucliadb/ingest/orm/utils.py +1 -1
  87. nucliadb/ingest/partitions.py +12 -1
  88. nucliadb/ingest/processing.py +17 -17
  89. nucliadb/ingest/serialize.py +202 -145
  90. nucliadb/ingest/service/writer.py +15 -114
  91. nucliadb/ingest/settings.py +36 -15
  92. nucliadb/ingest/utils.py +1 -2
  93. nucliadb/learning_proxy.py +23 -26
  94. nucliadb/metrics_exporter.py +20 -6
  95. nucliadb/middleware/__init__.py +82 -1
  96. nucliadb/migrator/datamanager.py +4 -11
  97. nucliadb/migrator/migrator.py +1 -2
  98. nucliadb/migrator/models.py +1 -2
  99. nucliadb/migrator/settings.py +1 -2
  100. nucliadb/models/internal/augment.py +614 -0
  101. nucliadb/models/internal/processing.py +19 -19
  102. nucliadb/openapi.py +2 -2
  103. nucliadb/purge/__init__.py +3 -8
  104. nucliadb/purge/orphan_shards.py +1 -2
  105. nucliadb/reader/__init__.py +5 -0
  106. nucliadb/reader/api/models.py +6 -13
  107. nucliadb/reader/api/v1/download.py +59 -38
  108. nucliadb/reader/api/v1/export_import.py +4 -4
  109. nucliadb/reader/api/v1/knowledgebox.py +37 -9
  110. nucliadb/reader/api/v1/learning_config.py +33 -14
  111. nucliadb/reader/api/v1/resource.py +61 -9
  112. nucliadb/reader/api/v1/services.py +18 -14
  113. nucliadb/reader/app.py +3 -1
  114. nucliadb/reader/reader/notifications.py +1 -2
  115. nucliadb/search/api/v1/__init__.py +3 -0
  116. nucliadb/search/api/v1/ask.py +3 -4
  117. nucliadb/search/api/v1/augment.py +585 -0
  118. nucliadb/search/api/v1/catalog.py +15 -19
  119. nucliadb/search/api/v1/find.py +16 -22
  120. nucliadb/search/api/v1/hydrate.py +328 -0
  121. nucliadb/search/api/v1/knowledgebox.py +1 -2
  122. nucliadb/search/api/v1/predict_proxy.py +1 -2
  123. nucliadb/search/api/v1/resource/ask.py +28 -8
  124. nucliadb/search/api/v1/resource/ingestion_agents.py +5 -6
  125. nucliadb/search/api/v1/resource/search.py +9 -11
  126. nucliadb/search/api/v1/retrieve.py +130 -0
  127. nucliadb/search/api/v1/search.py +28 -32
  128. nucliadb/search/api/v1/suggest.py +11 -14
  129. nucliadb/search/api/v1/summarize.py +1 -2
  130. nucliadb/search/api/v1/utils.py +2 -2
  131. nucliadb/search/app.py +3 -2
  132. nucliadb/search/augmentor/__init__.py +21 -0
  133. nucliadb/search/augmentor/augmentor.py +232 -0
  134. nucliadb/search/augmentor/fields.py +704 -0
  135. nucliadb/search/augmentor/metrics.py +24 -0
  136. nucliadb/search/augmentor/paragraphs.py +334 -0
  137. nucliadb/search/augmentor/resources.py +238 -0
  138. nucliadb/search/augmentor/utils.py +33 -0
  139. nucliadb/search/lifecycle.py +3 -1
  140. nucliadb/search/predict.py +33 -19
  141. nucliadb/search/predict_models.py +8 -9
  142. nucliadb/search/requesters/utils.py +11 -10
  143. nucliadb/search/search/cache.py +19 -42
  144. nucliadb/search/search/chat/ask.py +131 -59
  145. nucliadb/search/search/chat/exceptions.py +3 -5
  146. nucliadb/search/search/chat/fetcher.py +201 -0
  147. nucliadb/search/search/chat/images.py +6 -4
  148. nucliadb/search/search/chat/old_prompt.py +1375 -0
  149. nucliadb/search/search/chat/parser.py +510 -0
  150. nucliadb/search/search/chat/prompt.py +563 -615
  151. nucliadb/search/search/chat/query.py +453 -32
  152. nucliadb/search/search/chat/rpc.py +85 -0
  153. nucliadb/search/search/fetch.py +3 -4
  154. nucliadb/search/search/filters.py +8 -11
  155. nucliadb/search/search/find.py +33 -31
  156. nucliadb/search/search/find_merge.py +124 -331
  157. nucliadb/search/search/graph_strategy.py +14 -12
  158. nucliadb/search/search/hydrator/__init__.py +49 -0
  159. nucliadb/search/search/hydrator/fields.py +217 -0
  160. nucliadb/search/search/hydrator/images.py +130 -0
  161. nucliadb/search/search/hydrator/paragraphs.py +323 -0
  162. nucliadb/search/search/hydrator/resources.py +60 -0
  163. nucliadb/search/search/ingestion_agents.py +5 -5
  164. nucliadb/search/search/merge.py +90 -94
  165. nucliadb/search/search/metrics.py +24 -7
  166. nucliadb/search/search/paragraphs.py +7 -9
  167. nucliadb/search/search/predict_proxy.py +44 -18
  168. nucliadb/search/search/query.py +14 -86
  169. nucliadb/search/search/query_parser/fetcher.py +51 -82
  170. nucliadb/search/search/query_parser/models.py +19 -48
  171. nucliadb/search/search/query_parser/old_filters.py +20 -19
  172. nucliadb/search/search/query_parser/parsers/ask.py +5 -6
  173. nucliadb/search/search/query_parser/parsers/catalog.py +7 -11
  174. nucliadb/search/search/query_parser/parsers/common.py +21 -13
  175. nucliadb/search/search/query_parser/parsers/find.py +6 -29
  176. nucliadb/search/search/query_parser/parsers/graph.py +18 -28
  177. nucliadb/search/search/query_parser/parsers/retrieve.py +207 -0
  178. nucliadb/search/search/query_parser/parsers/search.py +15 -56
  179. nucliadb/search/search/query_parser/parsers/unit_retrieval.py +8 -29
  180. nucliadb/search/search/rank_fusion.py +18 -13
  181. nucliadb/search/search/rerankers.py +6 -7
  182. nucliadb/search/search/retrieval.py +300 -0
  183. nucliadb/search/search/summarize.py +5 -6
  184. nucliadb/search/search/utils.py +3 -4
  185. nucliadb/search/settings.py +1 -2
  186. nucliadb/standalone/api_router.py +1 -1
  187. nucliadb/standalone/app.py +4 -3
  188. nucliadb/standalone/auth.py +5 -6
  189. nucliadb/standalone/lifecycle.py +2 -2
  190. nucliadb/standalone/run.py +5 -4
  191. nucliadb/standalone/settings.py +5 -6
  192. nucliadb/standalone/versions.py +3 -4
  193. nucliadb/tasks/consumer.py +13 -8
  194. nucliadb/tasks/models.py +2 -1
  195. nucliadb/tasks/producer.py +3 -3
  196. nucliadb/tasks/retries.py +8 -7
  197. nucliadb/train/api/utils.py +1 -3
  198. nucliadb/train/api/v1/shards.py +1 -2
  199. nucliadb/train/api/v1/trainset.py +1 -2
  200. nucliadb/train/app.py +1 -1
  201. nucliadb/train/generator.py +4 -4
  202. nucliadb/train/generators/field_classifier.py +2 -2
  203. nucliadb/train/generators/field_streaming.py +6 -6
  204. nucliadb/train/generators/image_classifier.py +2 -2
  205. nucliadb/train/generators/paragraph_classifier.py +2 -2
  206. nucliadb/train/generators/paragraph_streaming.py +2 -2
  207. nucliadb/train/generators/question_answer_streaming.py +2 -2
  208. nucliadb/train/generators/sentence_classifier.py +4 -10
  209. nucliadb/train/generators/token_classifier.py +3 -2
  210. nucliadb/train/generators/utils.py +6 -5
  211. nucliadb/train/nodes.py +3 -3
  212. nucliadb/train/resource.py +6 -8
  213. nucliadb/train/settings.py +3 -4
  214. nucliadb/train/types.py +11 -11
  215. nucliadb/train/upload.py +3 -2
  216. nucliadb/train/uploader.py +1 -2
  217. nucliadb/train/utils.py +1 -2
  218. nucliadb/writer/api/v1/export_import.py +4 -1
  219. nucliadb/writer/api/v1/field.py +15 -14
  220. nucliadb/writer/api/v1/knowledgebox.py +18 -56
  221. nucliadb/writer/api/v1/learning_config.py +5 -4
  222. nucliadb/writer/api/v1/resource.py +9 -20
  223. nucliadb/writer/api/v1/services.py +10 -132
  224. nucliadb/writer/api/v1/upload.py +73 -72
  225. nucliadb/writer/app.py +8 -2
  226. nucliadb/writer/resource/basic.py +12 -15
  227. nucliadb/writer/resource/field.py +43 -5
  228. nucliadb/writer/resource/origin.py +7 -0
  229. nucliadb/writer/settings.py +2 -3
  230. nucliadb/writer/tus/__init__.py +2 -3
  231. nucliadb/writer/tus/azure.py +5 -7
  232. nucliadb/writer/tus/dm.py +3 -3
  233. nucliadb/writer/tus/exceptions.py +3 -4
  234. nucliadb/writer/tus/gcs.py +15 -22
  235. nucliadb/writer/tus/s3.py +2 -3
  236. nucliadb/writer/tus/storage.py +3 -3
  237. {nucliadb-6.7.2.post4874.dist-info → nucliadb-6.10.0.post5705.dist-info}/METADATA +10 -11
  238. nucliadb-6.10.0.post5705.dist-info/RECORD +410 -0
  239. nucliadb/common/datamanagers/entities.py +0 -139
  240. nucliadb/common/external_index_providers/pinecone.py +0 -894
  241. nucliadb/ingest/orm/processor/pgcatalog.py +0 -129
  242. nucliadb/search/search/hydrator.py +0 -197
  243. nucliadb-6.7.2.post4874.dist-info/RECORD +0 -383
  244. {nucliadb-6.7.2.post4874.dist-info → nucliadb-6.10.0.post5705.dist-info}/WHEEL +0 -0
  245. {nucliadb-6.7.2.post4874.dist-info → nucliadb-6.10.0.post5705.dist-info}/entry_points.txt +0 -0
  246. {nucliadb-6.7.2.post4874.dist-info → nucliadb-6.10.0.post5705.dist-info}/top_level.txt +0 -0
@@ -20,18 +20,22 @@
20
20
  import dataclasses
21
21
  import functools
22
22
  import json
23
- from typing import AsyncGenerator, Optional, cast
23
+ from collections.abc import AsyncGenerator
24
+ from typing import cast
24
25
 
25
26
  from nuclia_models.common.consumption import Consumption
26
27
  from nuclia_models.predict.generative_responses import (
27
28
  CitationsGenerativeResponse,
29
+ FootnoteCitationsGenerativeResponse,
28
30
  GenerativeChunk,
29
31
  JSONGenerativeResponse,
30
32
  MetaGenerativeResponse,
33
+ ReasoningGenerativeResponse,
31
34
  StatusGenerativeResponse,
32
35
  TextGenerativeResponse,
33
36
  )
34
37
  from pydantic_core import ValidationError
38
+ from typing_extensions import assert_never
35
39
 
36
40
  from nucliadb.common.datamanagers.exceptions import KnowledgeBoxNotFound
37
41
  from nucliadb.common.exceptions import InvalidQueryError
@@ -47,11 +51,13 @@ from nucliadb.search.search.chat.exceptions import (
47
51
  AnswerJsonSchemaTooLong,
48
52
  NoRetrievalResultsError,
49
53
  )
54
+ from nucliadb.search.search.chat.old_prompt import PromptContextBuilder as OldPromptContextBuilder
50
55
  from nucliadb.search.search.chat.prompt import PromptContextBuilder
51
56
  from nucliadb.search.search.chat.query import (
52
57
  NOT_ENOUGH_CONTEXT_ANSWER,
53
58
  ChatAuditor,
54
59
  add_resource_filter,
60
+ get_answer_stream,
55
61
  get_find_results,
56
62
  get_relations_results,
57
63
  maybe_audit_chat,
@@ -67,11 +73,15 @@ from nucliadb.search.search.metrics import AskMetrics, Metrics
67
73
  from nucliadb.search.search.query_parser.fetcher import Fetcher
68
74
  from nucliadb.search.search.query_parser.parsers.ask import fetcher_for_ask, parse_ask
69
75
  from nucliadb.search.search.rank_fusion import WeightedCombSum
70
- from nucliadb.search.search.rerankers import (
71
- get_reranker,
76
+ from nucliadb_models.retrieval import (
77
+ GraphScore,
78
+ KeywordScore,
79
+ RerankerScore,
80
+ RrfScore,
81
+ SemanticScore,
72
82
  )
73
- from nucliadb.search.utilities import get_predict
74
83
  from nucliadb_models.search import (
84
+ SCORE_TYPE,
75
85
  AnswerAskResponseItem,
76
86
  AskRequest,
77
87
  AskResponseItem,
@@ -90,6 +100,7 @@ from nucliadb_models.search import (
90
100
  FindOptions,
91
101
  FindParagraph,
92
102
  FindRequest,
103
+ FootnoteCitationsAskResponseItem,
93
104
  GraphStrategy,
94
105
  JSONAskResponseItem,
95
106
  KnowledgeboxFindResults,
@@ -102,6 +113,7 @@ from nucliadb_models.search import (
102
113
  PromptContext,
103
114
  PromptContextOrder,
104
115
  RagStrategyName,
116
+ ReasoningAskResponseItem,
105
117
  Relations,
106
118
  RelationsAskResponseItem,
107
119
  RetrievalAskResponseItem,
@@ -114,7 +126,9 @@ from nucliadb_models.search import (
114
126
  parse_rephrase_prompt,
115
127
  )
116
128
  from nucliadb_telemetry import errors
129
+ from nucliadb_utils import const
117
130
  from nucliadb_utils.exceptions import LimitsExceededError
131
+ from nucliadb_utils.utilities import has_feature
118
132
 
119
133
 
120
134
  @dataclasses.dataclass
@@ -128,7 +142,7 @@ class RetrievalResults:
128
142
  main_query: KnowledgeboxFindResults
129
143
  fetcher: Fetcher
130
144
  main_query_weight: float
131
- prequeries: Optional[list[PreQueryResult]] = None
145
+ prequeries: list[PreQueryResult] | None = None
132
146
  best_matches: list[RetrievalMatch] = dataclasses.field(default_factory=list)
133
147
 
134
148
 
@@ -139,15 +153,15 @@ class AskResult:
139
153
  kbid: str,
140
154
  ask_request: AskRequest,
141
155
  main_results: KnowledgeboxFindResults,
142
- prequeries_results: Optional[list[PreQueryResult]],
143
- nuclia_learning_id: Optional[str],
144
- predict_answer_stream: Optional[AsyncGenerator[GenerativeChunk, None]],
156
+ prequeries_results: list[PreQueryResult] | None,
157
+ nuclia_learning_id: str | None,
158
+ predict_answer_stream: AsyncGenerator[GenerativeChunk, None] | None,
145
159
  prompt_context: PromptContext,
146
160
  prompt_context_order: PromptContextOrder,
147
161
  auditor: ChatAuditor,
148
162
  metrics: AskMetrics,
149
163
  best_matches: list[RetrievalMatch],
150
- debug_chat_model: Optional[ChatModel],
164
+ debug_chat_model: ChatModel | None,
151
165
  augmented_context: AugmentedContext,
152
166
  ):
153
167
  # Initial attributes
@@ -167,12 +181,14 @@ class AskResult:
167
181
 
168
182
  # Computed from the predict chat answer stream
169
183
  self._answer_text = ""
170
- self._object: Optional[JSONGenerativeResponse] = None
171
- self._status: Optional[StatusGenerativeResponse] = None
172
- self._citations: Optional[CitationsGenerativeResponse] = None
173
- self._metadata: Optional[MetaGenerativeResponse] = None
174
- self._relations: Optional[Relations] = None
175
- self._consumption: Optional[Consumption] = None
184
+ self._reasoning_text: str | None = None
185
+ self._object: JSONGenerativeResponse | None = None
186
+ self._status: StatusGenerativeResponse | None = None
187
+ self._citations: CitationsGenerativeResponse | None = None
188
+ self._footnote_citations: FootnoteCitationsGenerativeResponse | None = None
189
+ self._metadata: MetaGenerativeResponse | None = None
190
+ self._relations: Relations | None = None
191
+ self._consumption: Consumption | None = None
176
192
 
177
193
  @property
178
194
  def status_code(self) -> AnswerStatusCode:
@@ -181,7 +197,7 @@ class AskResult:
181
197
  return AnswerStatusCode(self._status.code)
182
198
 
183
199
  @property
184
- def status_error_details(self) -> Optional[str]:
200
+ def status_error_details(self) -> str | None:
185
201
  if self._status is None: # pragma: no cover
186
202
  return None
187
203
  return self._status.details
@@ -220,12 +236,21 @@ class AskResult:
220
236
  async def _stream(self) -> AsyncGenerator[AskResponseItemType, None]:
221
237
  # First, stream out the predict answer
222
238
  first_chunk_yielded = False
239
+ first_reasoning_chunk_yielded = False
223
240
  with self.metrics.time("stream_predict_answer"):
224
241
  async for answer_chunk in self._stream_predict_answer_text():
225
- yield AnswerAskResponseItem(text=answer_chunk)
226
- if not first_chunk_yielded:
227
- self.metrics.record_first_chunk_yielded()
228
- first_chunk_yielded = True
242
+ if isinstance(answer_chunk, TextGenerativeResponse):
243
+ yield AnswerAskResponseItem(text=answer_chunk.text)
244
+ if not first_chunk_yielded:
245
+ self.metrics.record_first_chunk_yielded()
246
+ first_chunk_yielded = True
247
+ elif isinstance(answer_chunk, ReasoningGenerativeResponse):
248
+ yield ReasoningAskResponseItem(text=answer_chunk.text)
249
+ if not first_reasoning_chunk_yielded:
250
+ self.metrics.record_first_reasoning_chunk_yielded()
251
+ first_reasoning_chunk_yielded = True
252
+ else:
253
+ assert_never(answer_chunk)
229
254
 
230
255
  if self._object is not None:
231
256
  yield JSONAskResponseItem(object=self._object.object)
@@ -274,8 +299,10 @@ class AskResult:
274
299
  audit_answer = json.dumps(self._object.object).encode("utf-8")
275
300
  self.auditor.audit(
276
301
  text_answer=audit_answer,
302
+ text_reasoning=self._reasoning_text,
277
303
  generative_answer_time=self.metrics["stream_predict_answer"],
278
304
  generative_answer_first_chunk_time=self.metrics.get_first_chunk_time() or 0,
305
+ generative_reasoning_first_chunk_time=self.metrics.get_first_reasoning_chunk_time(),
279
306
  rephrase_time=self.metrics.get("rephrase"),
280
307
  status_code=self.status_code,
281
308
  )
@@ -287,6 +314,11 @@ class AskResult:
287
314
  yield CitationsAskResponseItem(
288
315
  citations=self._citations.citations,
289
316
  )
317
+ # Stream out the footnote citations mapping
318
+ if self._footnote_citations is not None:
319
+ yield FootnoteCitationsAskResponseItem(
320
+ footnote_to_context=self._footnote_citations.footnote_to_context,
321
+ )
290
322
 
291
323
  # Stream out generic metadata about the answer
292
324
  if self._metadata is not None:
@@ -364,11 +396,15 @@ class AskResult:
364
396
  if self._citations is not None:
365
397
  citations = self._citations.citations
366
398
 
399
+ footnote_citations = {}
400
+ if self._footnote_citations is not None:
401
+ footnote_citations = self._footnote_citations.footnote_to_context
402
+
367
403
  answer_json = None
368
404
  if self._object is not None:
369
405
  answer_json = self._object.object
370
406
 
371
- prequeries_results: Optional[dict[str, KnowledgeboxFindResults]] = None
407
+ prequeries_results: dict[str, KnowledgeboxFindResults] | None = None
372
408
  if self.prequeries_results:
373
409
  prequeries_results = {}
374
410
  for index, (prequery, result) in enumerate(self.prequeries_results):
@@ -384,6 +420,7 @@ class AskResult:
384
420
 
385
421
  response = SyncAskResponse(
386
422
  answer=self._answer_text,
423
+ reasoning=self._reasoning_text,
387
424
  answer_json=answer_json,
388
425
  status=self.status_code.prettify(),
389
426
  relations=self._relations,
@@ -391,6 +428,7 @@ class AskResult:
391
428
  retrieval_best_matches=best_matches,
392
429
  prequeries=prequeries_results,
393
430
  citations=citations,
431
+ citation_footnote_to_context=footnote_citations,
394
432
  metadata=metadata,
395
433
  consumption=self._consumption,
396
434
  learning_id=self.nuclia_learning_id or "",
@@ -420,7 +458,9 @@ class AskResult:
420
458
  )
421
459
  return self._relations
422
460
 
423
- async def _stream_predict_answer_text(self) -> AsyncGenerator[str, None]:
461
+ async def _stream_predict_answer_text(
462
+ self,
463
+ ) -> AsyncGenerator[TextGenerativeResponse | ReasoningGenerativeResponse, None]:
424
464
  """
425
465
  Reads the stream of the generative model, yielding the answer text but also parsing
426
466
  other items like status codes, citations and miscellaneous metadata.
@@ -435,13 +475,21 @@ class AskResult:
435
475
  item = generative_chunk.chunk
436
476
  if isinstance(item, TextGenerativeResponse):
437
477
  self._answer_text += item.text
438
- yield item.text
478
+ yield item
479
+ elif isinstance(item, ReasoningGenerativeResponse):
480
+ if self._reasoning_text is None:
481
+ self._reasoning_text = item.text
482
+ else:
483
+ self._reasoning_text += item.text
484
+ yield item
439
485
  elif isinstance(item, JSONGenerativeResponse):
440
486
  self._object = item
441
487
  elif isinstance(item, StatusGenerativeResponse):
442
488
  self._status = item
443
489
  elif isinstance(item, CitationsGenerativeResponse):
444
490
  self._citations = item
491
+ elif isinstance(item, FootnoteCitationsGenerativeResponse):
492
+ self._footnote_citations = item
445
493
  elif isinstance(item, MetaGenerativeResponse):
446
494
  self._metadata = item
447
495
  elif isinstance(item, Consumption):
@@ -456,8 +504,8 @@ class AskResult:
456
504
  class NotEnoughContextAskResult(AskResult):
457
505
  def __init__(
458
506
  self,
459
- main_results: Optional[KnowledgeboxFindResults] = None,
460
- prequeries_results: Optional[list[PreQueryResult]] = None,
507
+ main_results: KnowledgeboxFindResults | None = None,
508
+ prequeries_results: list[PreQueryResult] | None = None,
461
509
  ):
462
510
  self.main_results = main_results or KnowledgeboxFindResults(resources={}, min_score=None)
463
511
  self.prequeries_results = prequeries_results or []
@@ -507,8 +555,8 @@ async def ask(
507
555
  user_id: str,
508
556
  client_type: NucliaDBClientType,
509
557
  origin: str,
510
- resource: Optional[str] = None,
511
- extra_predict_headers: Optional[dict[str, str]] = None,
558
+ resource: str | None = None,
559
+ extra_predict_headers: dict[str, str] | None = None,
512
560
  ) -> AskResult:
513
561
  metrics = AskMetrics()
514
562
  chat_history = ask_request.chat_history or []
@@ -559,11 +607,13 @@ async def ask(
559
607
  origin=origin,
560
608
  generative_answer_time=0,
561
609
  generative_answer_first_chunk_time=0,
610
+ generative_reasoning_first_chunk_time=None,
562
611
  rephrase_time=metrics.get("rephrase"),
563
612
  user_query=user_query,
564
613
  rephrased_query=rephrased_query,
565
614
  retrieval_rephrase_query=err.main_query.rephrased_query if err.main_query else None,
566
615
  text_answer=b"",
616
+ text_reasoning=None,
567
617
  status_code=AnswerStatusCode.NO_RETRIEVAL_DATA,
568
618
  chat_history=chat_history,
569
619
  query_context={},
@@ -585,19 +635,36 @@ async def ask(
585
635
 
586
636
  # Now we build the prompt context
587
637
  with metrics.time("context_building"):
588
- prompt_context_builder = PromptContextBuilder(
589
- kbid=kbid,
590
- ordered_paragraphs=[match.paragraph for match in retrieval_results.best_matches],
591
- resource=resource,
592
- user_context=user_context,
593
- user_image_context=ask_request.extra_context_images,
594
- strategies=ask_request.rag_strategies,
595
- image_strategies=ask_request.rag_images_strategies,
596
- max_context_characters=tokens_to_chars(generation.max_context_tokens),
597
- visual_llm=generation.use_visual_llm,
598
- query_image=ask_request.query_image,
599
- metrics=metrics.child_span("context_building"),
600
- )
638
+ prompt_context_builder: PromptContextBuilder | OldPromptContextBuilder
639
+ if has_feature(const.Features.ASK_DECOUPLED, context={"kbid": kbid}):
640
+ prompt_context_builder = PromptContextBuilder(
641
+ kbid=kbid,
642
+ ordered_paragraphs=[match.paragraph for match in retrieval_results.best_matches],
643
+ resource=resource,
644
+ user_context=user_context,
645
+ user_image_context=ask_request.extra_context_images,
646
+ strategies=ask_request.rag_strategies,
647
+ image_strategies=ask_request.rag_images_strategies,
648
+ max_context_characters=tokens_to_chars(generation.max_context_tokens),
649
+ visual_llm=generation.use_visual_llm,
650
+ query_image=ask_request.query_image,
651
+ metrics=metrics.child_span("context_building"),
652
+ )
653
+ else:
654
+ prompt_context_builder = OldPromptContextBuilder(
655
+ kbid=kbid,
656
+ ordered_paragraphs=[match.paragraph for match in retrieval_results.best_matches],
657
+ resource=resource,
658
+ user_context=user_context,
659
+ user_image_context=ask_request.extra_context_images,
660
+ strategies=ask_request.rag_strategies,
661
+ image_strategies=ask_request.rag_images_strategies,
662
+ max_context_characters=tokens_to_chars(generation.max_context_tokens),
663
+ visual_llm=generation.use_visual_llm,
664
+ query_image=ask_request.query_image,
665
+ metrics=metrics.child_span("context_building"),
666
+ )
667
+
601
668
  (
602
669
  prompt_context,
603
670
  prompt_context_order,
@@ -625,6 +692,7 @@ async def ask(
625
692
  json_schema=ask_request.answer_json_schema,
626
693
  rerank_context=False,
627
694
  top_k=ask_request.top_k,
695
+ reasoning=ask_request.reasoning,
628
696
  )
629
697
 
630
698
  nuclia_learning_id = None
@@ -632,14 +700,11 @@ async def ask(
632
700
  predict_answer_stream = None
633
701
  if ask_request.generate_answer:
634
702
  with metrics.time("stream_start"):
635
- predict = get_predict()
636
703
  (
637
704
  nuclia_learning_id,
638
705
  nuclia_learning_model,
639
706
  predict_answer_stream,
640
- ) = await predict.chat_query_ndjson(
641
- kbid=kbid, item=chat_model, extra_headers=extra_predict_headers
642
- )
707
+ ) = await get_answer_stream(kbid=kbid, item=chat_model, extra_headers=extra_predict_headers)
643
708
 
644
709
  auditor = ChatAuditor(
645
710
  kbid=kbid,
@@ -714,7 +779,7 @@ def handled_ask_exceptions(func):
714
779
  return wrapper
715
780
 
716
781
 
717
- def parse_prequeries(ask_request: AskRequest) -> Optional[PreQueriesStrategy]:
782
+ def parse_prequeries(ask_request: AskRequest) -> PreQueriesStrategy | None:
718
783
  query_ids = []
719
784
  for rag_strategy in ask_request.rag_strategies:
720
785
  if rag_strategy.name == RagStrategyName.PREQUERIES:
@@ -733,7 +798,7 @@ def parse_prequeries(ask_request: AskRequest) -> Optional[PreQueriesStrategy]:
733
798
  return None
734
799
 
735
800
 
736
- def parse_graph_strategy(ask_request: AskRequest) -> Optional[GraphStrategy]:
801
+ def parse_graph_strategy(ask_request: AskRequest) -> GraphStrategy | None:
737
802
  for rag_strategy in ask_request.rag_strategies:
738
803
  if rag_strategy.name == RagStrategyName.GRAPH:
739
804
  return cast(GraphStrategy, rag_strategy)
@@ -748,7 +813,7 @@ async def retrieval_step(
748
813
  user_id: str,
749
814
  origin: str,
750
815
  metrics: Metrics,
751
- resource: Optional[str] = None,
816
+ resource: str | None = None,
752
817
  ) -> RetrievalResults:
753
818
  """
754
819
  This function encapsulates all the logic related to retrieval in the ask endpoint.
@@ -787,7 +852,7 @@ async def retrieval_in_kb(
787
852
  ) -> RetrievalResults:
788
853
  prequeries = parse_prequeries(ask_request)
789
854
  graph_strategy = parse_graph_strategy(ask_request)
790
- main_results, prequeries_results, parsed_query = await get_find_results(
855
+ main_results, prequeries_results, fetcher, reranker = await get_find_results(
791
856
  kbid=kbid,
792
857
  query=main_query,
793
858
  item=ask_request,
@@ -799,10 +864,6 @@ async def retrieval_in_kb(
799
864
  )
800
865
 
801
866
  if graph_strategy is not None:
802
- assert parsed_query.retrieval.reranker is not None, (
803
- "find parser must provide a reranking algorithm"
804
- )
805
- reranker = get_reranker(parsed_query.retrieval.reranker)
806
867
  graph_results, graph_request = await get_graph_results(
807
868
  kbid=kbid,
808
869
  query=main_query,
@@ -835,7 +896,7 @@ async def retrieval_in_kb(
835
896
  return RetrievalResults(
836
897
  main_query=main_results,
837
898
  prequeries=prequeries_results,
838
- fetcher=parsed_query.fetcher,
899
+ fetcher=fetcher,
839
900
  main_query_weight=main_query_weight,
840
901
  best_matches=best_matches,
841
902
  )
@@ -875,7 +936,7 @@ async def retrieval_in_resource(
875
936
  )
876
937
  add_resource_filter(prequery.request, [resource])
877
938
 
878
- main_results, prequeries_results, parsed_query = await get_find_results(
939
+ main_results, prequeries_results, fetcher, _ = await get_find_results(
879
940
  kbid=kbid,
880
941
  query=main_query,
881
942
  item=ask_request,
@@ -898,7 +959,7 @@ async def retrieval_in_resource(
898
959
  return RetrievalResults(
899
960
  main_query=main_results,
900
961
  prequeries=prequeries_results,
901
- fetcher=parsed_query.fetcher,
962
+ fetcher=fetcher,
902
963
  main_query_weight=main_query_weight,
903
964
  best_matches=best_matches,
904
965
  )
@@ -910,7 +971,7 @@ class _FindParagraph(ScoredTextBlock):
910
971
 
911
972
  def compute_best_matches(
912
973
  main_results: KnowledgeboxFindResults,
913
- prequeries_results: Optional[list[PreQueryResult]] = None,
974
+ prequeries_results: list[PreQueryResult] | None = None,
914
975
  main_query_weight: float = 1.0,
915
976
  ) -> list[RetrievalMatch]:
916
977
  """
@@ -925,15 +986,27 @@ def compute_best_matches(
925
986
  `main_query_weight` is the weight given to the paragraphs matching the main query when calculating the final score.
926
987
  """
927
988
 
989
+ score_type_map = {
990
+ SCORE_TYPE.VECTOR: SemanticScore,
991
+ SCORE_TYPE.BM25: KeywordScore,
992
+ SCORE_TYPE.BOTH: RrfScore, # /find only exposes RRF as rank fusion algorithm
993
+ SCORE_TYPE.RERANKER: RerankerScore,
994
+ SCORE_TYPE.RELATION_RELEVANCE: GraphScore,
995
+ }
996
+
928
997
  def extract_paragraphs(results: KnowledgeboxFindResults) -> list[_FindParagraph]:
929
998
  paragraphs = []
930
999
  for resource in results.resources.values():
931
1000
  for field in resource.fields.values():
932
1001
  for paragraph in field.paragraphs.values():
1002
+ # TODO(decoupled-ask): we don't know the score history, as
1003
+ # we are using find results. Once we move boolean queries
1004
+ # inside the new retrieval flow we'll move this and have the
1005
+ # proper information to do this rank fusion
933
1006
  paragraphs.append(
934
1007
  _FindParagraph(
935
1008
  paragraph_id=ParagraphId.from_string(paragraph.id),
936
- score=paragraph.score,
1009
+ scores=[score_type_map[paragraph.score_type](score=paragraph.score)],
937
1010
  score_type=paragraph.score_type,
938
1011
  original=paragraph,
939
1012
  )
@@ -969,7 +1042,7 @@ def compute_best_matches(
969
1042
 
970
1043
  def calculate_prequeries_for_json_schema(
971
1044
  ask_request: AskRequest,
972
- ) -> Optional[PreQueriesStrategy]:
1045
+ ) -> PreQueriesStrategy | None:
973
1046
  """
974
1047
  This function generates a PreQueriesStrategy with a query for each property in the JSON schema
975
1048
  found in ask_request.answer_json_schema.
@@ -1034,7 +1107,6 @@ def calculate_prequeries_for_json_schema(
1034
1107
  rephrase=ask_request.rephrase,
1035
1108
  rephrase_prompt=parse_rephrase_prompt(ask_request),
1036
1109
  security=ask_request.security,
1037
- autofilter=False,
1038
1110
  )
1039
1111
  prequery = PreQuery(
1040
1112
  request=req,
@@ -19,17 +19,15 @@
19
19
  #
20
20
 
21
21
 
22
- from typing import Optional
23
-
24
22
  from nucliadb_models.search import KnowledgeboxFindResults, PreQueryResult
25
23
 
26
24
 
27
25
  class NoRetrievalResultsError(Exception):
28
26
  def __init__(
29
27
  self,
30
- main: Optional[KnowledgeboxFindResults] = None,
31
- prequeries: Optional[list[PreQueryResult]] = None,
32
- prefilters: Optional[list[PreQueryResult]] = None,
28
+ main: KnowledgeboxFindResults | None = None,
29
+ prequeries: list[PreQueryResult] | None = None,
30
+ prefilters: list[PreQueryResult] | None = None,
33
31
  ):
34
32
  self.main_query = main
35
33
  self.prequeries = prequeries