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,310 @@
|
|
|
1
|
+
from asyncio import gather
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from time import time
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from hyperforge.manager import Manager
|
|
7
|
+
from hyperforge.memory.memory import QuestionMemory, Source
|
|
8
|
+
from nuclia_models.predict.run_agents import RunTextAgentsRequest
|
|
9
|
+
from nuclia_models.worker.triggers import Relation
|
|
10
|
+
from nucliadb_models import RelationNodeType
|
|
11
|
+
from nucliadb_models.search import Filter, KnowledgeGraphEntity
|
|
12
|
+
|
|
13
|
+
from hyperforge import PROMPT_ENVIRONMENT
|
|
14
|
+
from hyperforge_nucliadb.ask.config import AskAgentConfig
|
|
15
|
+
from hyperforge_nucliadb.ask.kb_analysis import get_knowledge_base_analysis
|
|
16
|
+
from hyperforge_nucliadb.ask.models import Analysis
|
|
17
|
+
from hyperforge_nucliadb.ask.prompt_analysis import configure_prompts
|
|
18
|
+
from hyperforge_nucliadb.ask.query_analysis import pre_query_analysis
|
|
19
|
+
from hyperforge_nucliadb.driver import NucliaDBDriver
|
|
20
|
+
|
|
21
|
+
KEYWORD_FILTER_PROMPT = "Extract if any the keywords that should appear on the retrieved results and its a must match, make sure that are keywords that are not common words, and that are not the same as the question or answer. Please define ONLY the keywords without any explanation. Only one or two words maximum. JUST A LIST OF KEYWORDS. DO NOT ADD ANY EXTRA NOTES AT THE END, JUST A LIST OF KEYWORDS"
|
|
22
|
+
|
|
23
|
+
REPHRASE_SEMANTIC_PROMPT = "Rephrase this question so its better for semantic retrieval, and keep the rephrased question in the same language as the original. Please define ONLY the question without any explanation. JUST A SENTENCE. DO NOT ADD ANY EXTRA NOTES AT THE END, JUST ONE SENTENCE"
|
|
24
|
+
|
|
25
|
+
REPHRASE_LEXICAL_PROMPT = "Rephrase this question so its better for lexical retrieval, and keep the rephrased question in the same language as the original. Please define ONLY the question without any explanation. JUST A SENTENCE. DO NOT ADD ANY EXTRA NOTES AT THE END, JUST ONE SENTENCE"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ASK_JSON_SCHEMA = {
|
|
29
|
+
"title": "ask_configuration",
|
|
30
|
+
"description": "Configuration extracted from reasoning engine",
|
|
31
|
+
"parameters": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"link": {
|
|
35
|
+
"type": "boolean",
|
|
36
|
+
"description": "The user wants link reference to the answer?",
|
|
37
|
+
},
|
|
38
|
+
"knowledge_scan": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "If the query requires a knowledge aggregation or scan search to answer define the entities, labels and relations to query in the KB. Example queries: How many ...",
|
|
41
|
+
},
|
|
42
|
+
"semantic_query": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": REPHRASE_SEMANTIC_PROMPT,
|
|
45
|
+
},
|
|
46
|
+
"lexical_query": {"type": "string", "description": REPHRASE_LEXICAL_PROMPT},
|
|
47
|
+
"visual": {
|
|
48
|
+
"type": "boolean",
|
|
49
|
+
"description": "Is required an analysis of an image to answer this question, answer with false or true",
|
|
50
|
+
},
|
|
51
|
+
"keywords_filter": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {"type": "string"},
|
|
54
|
+
"description": KEYWORD_FILTER_PROMPT,
|
|
55
|
+
},
|
|
56
|
+
"reason": {"type": "string"},
|
|
57
|
+
"entities": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"description": "Entities related to the user question to query in the KB",
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
"relations": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"description": "Relations related to the user question to query in the KB",
|
|
67
|
+
"items": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
"pre_queries": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
},
|
|
76
|
+
"description": "Pre queries to run before the main query to gather more information",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"required": [
|
|
80
|
+
"semantic_query",
|
|
81
|
+
"lexical_query",
|
|
82
|
+
"visual",
|
|
83
|
+
"keywords_filter",
|
|
84
|
+
"reason",
|
|
85
|
+
"pre_queries",
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
ASK_PROMPT = """
|
|
92
|
+
|
|
93
|
+
Information about the KB:
|
|
94
|
+
|
|
95
|
+
# {{sid}}
|
|
96
|
+
|
|
97
|
+
description: {{sdescription}}
|
|
98
|
+
|
|
99
|
+
And given the question: {{question}}
|
|
100
|
+
|
|
101
|
+
## labels: {{slabels}}
|
|
102
|
+
|
|
103
|
+
## Facets:
|
|
104
|
+
|
|
105
|
+
{{facets}}
|
|
106
|
+
|
|
107
|
+
# Important rules to follow
|
|
108
|
+
|
|
109
|
+
{% for rule in rules %}
|
|
110
|
+
{{rule}}
|
|
111
|
+
{% endfor -%}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
ASK_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(ASK_PROMPT)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def question_analysis(
|
|
120
|
+
memory: QuestionMemory,
|
|
121
|
+
manager: Manager,
|
|
122
|
+
nucliadb_driver: NucliaDBDriver,
|
|
123
|
+
source: Source,
|
|
124
|
+
config: AskAgentConfig,
|
|
125
|
+
question: str,
|
|
126
|
+
step_title: str = "Knowledge Box Ask: Choose parameters",
|
|
127
|
+
):
|
|
128
|
+
if question is None:
|
|
129
|
+
raise Exception("Question is None")
|
|
130
|
+
question = question.strip()
|
|
131
|
+
|
|
132
|
+
if source is None:
|
|
133
|
+
raise Exception("Source is None")
|
|
134
|
+
|
|
135
|
+
kb_analysis = get_knowledge_base_analysis(source)
|
|
136
|
+
|
|
137
|
+
query_analysis_result = pre_query_analysis(config, question, kb_analysis)
|
|
138
|
+
|
|
139
|
+
ask_json_schema_copy: Dict[str, Any] = deepcopy(ASK_JSON_SCHEMA)
|
|
140
|
+
configure_prompts(config, ask_json_schema_copy, kb_analysis, query_analysis_result)
|
|
141
|
+
|
|
142
|
+
facets = ""
|
|
143
|
+
facets += "## Content Types\n"
|
|
144
|
+
facets += "The following content types are available in the KB:\n\n"
|
|
145
|
+
for key, count in kb_analysis.content_types.items():
|
|
146
|
+
facets += f"- {key}: {count}\n"
|
|
147
|
+
|
|
148
|
+
facets += "The following languages are available in the KB:\n\n"
|
|
149
|
+
for key, count in kb_analysis.languages.items():
|
|
150
|
+
facets += f"- {key}: {count}\n"
|
|
151
|
+
|
|
152
|
+
prompt = ASK_PROMPT_TEMPLATE.render(
|
|
153
|
+
source=source,
|
|
154
|
+
rules=memory.get_rules(),
|
|
155
|
+
question=question,
|
|
156
|
+
facets=facets,
|
|
157
|
+
sid=source.id,
|
|
158
|
+
sdescription=source.description,
|
|
159
|
+
slabels=source.labels,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
t0 = time()
|
|
163
|
+
input_tokens = 0.0
|
|
164
|
+
output_tokens = 0.0
|
|
165
|
+
|
|
166
|
+
task1 = manager.execute_json(
|
|
167
|
+
user_id="arag-ask",
|
|
168
|
+
prompt=prompt,
|
|
169
|
+
model=config.configuration_model,
|
|
170
|
+
schema=ask_json_schema_copy,
|
|
171
|
+
tracking=memory.get_tracking_info(),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
task2 = nucliadb_driver.driver.run_text_agents(
|
|
175
|
+
kbid=nucliadb_driver.config.kbid,
|
|
176
|
+
content=RunTextAgentsRequest(user_id="arag-ask", texts=[question]),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
task3 = manager.tokens_predict(
|
|
180
|
+
text=question,
|
|
181
|
+
model=kb_analysis.entity_model,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Extract configuration from reasoning engine
|
|
185
|
+
(configuration_json, input, output), das_answer, tokens = await gather(
|
|
186
|
+
task1, task2, task3
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
input_tokens += input
|
|
190
|
+
output_tokens += output
|
|
191
|
+
|
|
192
|
+
entities_to_filter = deepcopy(config.query_entities)
|
|
193
|
+
relations_to_filter: List[Relation] = []
|
|
194
|
+
labels_to_filter: List[Filter] = []
|
|
195
|
+
if len(config.filters) > 0:
|
|
196
|
+
labels_to_filter.append(Filter(all=config.filters))
|
|
197
|
+
new_text_fields: List[str] = []
|
|
198
|
+
new_json_fields: Dict[str, Any] = {}
|
|
199
|
+
|
|
200
|
+
for detected_token in tokens.tokens:
|
|
201
|
+
entities_to_filter.append(
|
|
202
|
+
KnowledgeGraphEntity(
|
|
203
|
+
name=detected_token.text,
|
|
204
|
+
type=RelationNodeType.ENTITY,
|
|
205
|
+
subtype=detected_token.ner,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
for result in das_answer.results:
|
|
210
|
+
input_tokens += result.input_nuclia_tokens
|
|
211
|
+
output_tokens += result.output_nuclia_tokens
|
|
212
|
+
for payload in result.payloads:
|
|
213
|
+
# we could implement for guards
|
|
214
|
+
if payload.entities is not None:
|
|
215
|
+
for entity in payload.entities:
|
|
216
|
+
for entity_class, entity_value in entity.labels.items():
|
|
217
|
+
entities_to_filter.append(
|
|
218
|
+
KnowledgeGraphEntity(
|
|
219
|
+
name=entity_value,
|
|
220
|
+
type=RelationNodeType.ENTITY,
|
|
221
|
+
subtype=entity_class,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if payload.relations is not None:
|
|
226
|
+
for relations in payload.relations:
|
|
227
|
+
relations_to_filter.extend(relations.relations)
|
|
228
|
+
|
|
229
|
+
if payload.labels is not None:
|
|
230
|
+
das_filters: list[str] = []
|
|
231
|
+
for label in payload.labels:
|
|
232
|
+
if isinstance(label.labels, list):
|
|
233
|
+
for label_obj_str in label.labels:
|
|
234
|
+
das_filters.append(label_obj_str)
|
|
235
|
+
elif isinstance(label.labels, dict):
|
|
236
|
+
for key, value in label.labels.items():
|
|
237
|
+
das_filters.append(f"{key}:{value}")
|
|
238
|
+
else:
|
|
239
|
+
raise Exception(f"Unknown label type: {label.labels}")
|
|
240
|
+
labels_to_filter.append(Filter(all=das_filters))
|
|
241
|
+
|
|
242
|
+
if payload.asks is not None:
|
|
243
|
+
for ask in payload.asks:
|
|
244
|
+
if ask.empty is False and ask.text is not None:
|
|
245
|
+
new_text_fields.append(ask.text)
|
|
246
|
+
elif ask.empty is False and ask.json_output is not None:
|
|
247
|
+
new_json_fields.update(ask.json_output)
|
|
248
|
+
|
|
249
|
+
for entity_llm in configuration_json.get("entities", []):
|
|
250
|
+
entities_to_filter.append(
|
|
251
|
+
KnowledgeGraphEntity(
|
|
252
|
+
name=entity_llm,
|
|
253
|
+
type=RelationNodeType.ENTITY,
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if kb_analysis.default_semantic_model is None and (
|
|
258
|
+
kb_analysis.semantic_models is None or len(kb_analysis.semantic_models) == 0
|
|
259
|
+
):
|
|
260
|
+
raise Exception("No semantic model found in the knowledge base")
|
|
261
|
+
|
|
262
|
+
semantic_model = (
|
|
263
|
+
kb_analysis.default_semantic_model
|
|
264
|
+
if kb_analysis.default_semantic_model
|
|
265
|
+
else kb_analysis.semantic_models[0]
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
semantic_query = configuration_json["semantic_query"]
|
|
269
|
+
lexical_query = configuration_json["lexical_query"]
|
|
270
|
+
visual = configuration_json.get("visual", False)
|
|
271
|
+
link = configuration_json.get("link", False)
|
|
272
|
+
knowledge_graph = configuration_json.get("knowledge_graph", None)
|
|
273
|
+
keywords_filter: List[str] = configuration_json.get("keywords_filter", [])
|
|
274
|
+
if isinstance(keywords_filter, str):
|
|
275
|
+
keywords_filter = [word.strip() for word in keywords_filter.split(",")]
|
|
276
|
+
|
|
277
|
+
reason = configuration_json.get("reason")
|
|
278
|
+
pre_queries = configuration_json.get("pre_queries", [])
|
|
279
|
+
relations = configuration_json.get("relations", [])
|
|
280
|
+
await memory.add_step(
|
|
281
|
+
step_module="ask",
|
|
282
|
+
step_title=step_title,
|
|
283
|
+
step_agent_path=f"/context/{config.id if config.id else 'default'}",
|
|
284
|
+
step_value=str(configuration_json),
|
|
285
|
+
step_reason=reason if reason is not None else "",
|
|
286
|
+
timeit=time() - t0,
|
|
287
|
+
input_nuclia_tokens=input_tokens,
|
|
288
|
+
output_nuclia_tokens=output_tokens,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return Analysis(
|
|
292
|
+
semantic_query=semantic_query,
|
|
293
|
+
lexical_query=lexical_query,
|
|
294
|
+
visual=visual,
|
|
295
|
+
keywords_filter=keywords_filter,
|
|
296
|
+
reason=reason,
|
|
297
|
+
input_tokens=input_tokens,
|
|
298
|
+
output_tokens=output_tokens,
|
|
299
|
+
labels=labels_to_filter,
|
|
300
|
+
semantic_model=semantic_model,
|
|
301
|
+
entities=entities_to_filter,
|
|
302
|
+
pre_queries=pre_queries,
|
|
303
|
+
relations=relations_to_filter,
|
|
304
|
+
new_text_fields=new_text_fields,
|
|
305
|
+
new_json_fields=new_json_fields,
|
|
306
|
+
semantic_config=kb_analysis.semantic_model_configs[semantic_model],
|
|
307
|
+
top_k=config.top_k,
|
|
308
|
+
link=link,
|
|
309
|
+
knowledge_graph=knowledge_graph,
|
|
310
|
+
)
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from time import time
|
|
3
|
+
from typing import Any, ClassVar, Dict, List, Optional, cast
|
|
4
|
+
|
|
5
|
+
from hyperforge.agent import Agent
|
|
6
|
+
from hyperforge.configure import agent
|
|
7
|
+
from hyperforge.context.agent import ContextAgent, trace_agent
|
|
8
|
+
from hyperforge.definition import FunctionDefinition
|
|
9
|
+
from hyperforge.manager import Manager
|
|
10
|
+
from hyperforge.memory.memory import Context, QuestionMemory, Source
|
|
11
|
+
|
|
12
|
+
from hyperforge import logger
|
|
13
|
+
from hyperforge_nucliadb.ask.analysis import question_analysis
|
|
14
|
+
from hyperforge_nucliadb.ask.config import AskAgentConfig
|
|
15
|
+
from hyperforge_nucliadb.ask.hydrate import hydrate
|
|
16
|
+
from hyperforge_nucliadb.ask.knowledge_scan import knowledge_scan
|
|
17
|
+
from hyperforge_nucliadb.ask.models import Analysis
|
|
18
|
+
from hyperforge_nucliadb.ask.multi import choose_source
|
|
19
|
+
from hyperforge_nucliadb.ask.nucliadb import query_ndb, standard_query_ndb
|
|
20
|
+
from hyperforge_nucliadb.ask.rerank import rerank
|
|
21
|
+
from hyperforge_nucliadb.driver import NucliaDBDriver
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@agent(
|
|
25
|
+
id="ask",
|
|
26
|
+
agent_type="context",
|
|
27
|
+
title="Knowledge Box Ask",
|
|
28
|
+
description="Ask a question to the knowledge box and retrieve relevant information",
|
|
29
|
+
config_schema=AskAgentConfig,
|
|
30
|
+
)
|
|
31
|
+
class AskAgent(ContextAgent, Agent[AskAgentConfig]):
|
|
32
|
+
__published_functions__: ClassVar[Dict[str, FunctionDefinition]] = {
|
|
33
|
+
"search_by_title": FunctionDefinition(
|
|
34
|
+
name="search_by_title",
|
|
35
|
+
description="Search for context in the Knowledge Box by title. Useful for specific queries where the title is known.",
|
|
36
|
+
parameters={
|
|
37
|
+
"title": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "The title to search for in the Knowledge Box.",
|
|
40
|
+
},
|
|
41
|
+
"filters": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": {"type": "string"},
|
|
44
|
+
"description": "Filters to apply when searching in the Knowledge Box.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
"ask_analysis_query": FunctionDefinition(
|
|
49
|
+
name="ask_analysis_query",
|
|
50
|
+
description="Generate filters to be used when querying the Knowledge Box. Useful to refine search results based on the question.",
|
|
51
|
+
parameters={
|
|
52
|
+
"question": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "The question based on which to generate filters.",
|
|
55
|
+
},
|
|
56
|
+
"kbid": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "The Knowledge Box ID to use for generating the analysis. If not provided, the default sources will be used.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
"ask_agent": FunctionDefinition(
|
|
63
|
+
name="ask_agent",
|
|
64
|
+
description="Search for context in the Knowledge Box by title. Useful for specific queries where the title is known.",
|
|
65
|
+
parameters={
|
|
66
|
+
"question": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "The question to search for in the Knowledge Box.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fallback: Optional[ContextAgent] = None
|
|
75
|
+
|
|
76
|
+
async def ask_analysis_query(
|
|
77
|
+
self,
|
|
78
|
+
memory: QuestionMemory,
|
|
79
|
+
manager: Manager,
|
|
80
|
+
question: str,
|
|
81
|
+
**kwargs: Any,
|
|
82
|
+
) -> List[Analysis]:
|
|
83
|
+
sources = await self.get_source(question, memory, manager)
|
|
84
|
+
result = []
|
|
85
|
+
for source in sources:
|
|
86
|
+
nucliadb_driver: Optional[NucliaDBDriver] = cast(
|
|
87
|
+
NucliaDBDriver, manager.drivers.get(source.id)
|
|
88
|
+
)
|
|
89
|
+
if nucliadb_driver is None:
|
|
90
|
+
raise Exception("No NDB available")
|
|
91
|
+
analysis = await question_analysis(
|
|
92
|
+
memory,
|
|
93
|
+
manager,
|
|
94
|
+
nucliadb_driver,
|
|
95
|
+
source,
|
|
96
|
+
config=self.config,
|
|
97
|
+
question=question,
|
|
98
|
+
step_title=self.step_title("Choose parameters"),
|
|
99
|
+
) # type: ignore
|
|
100
|
+
result.append(analysis)
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
async def work(
|
|
104
|
+
self,
|
|
105
|
+
memory: QuestionMemory,
|
|
106
|
+
manager: Manager,
|
|
107
|
+
context: Context,
|
|
108
|
+
nucliadb_driver: NucliaDBDriver,
|
|
109
|
+
source: Source,
|
|
110
|
+
question: str,
|
|
111
|
+
):
|
|
112
|
+
# Generate semantic questions required to answer
|
|
113
|
+
analysis: Optional[Analysis] = None
|
|
114
|
+
if self.config.ai_parameter_search:
|
|
115
|
+
try:
|
|
116
|
+
analysis = await question_analysis(
|
|
117
|
+
memory,
|
|
118
|
+
manager,
|
|
119
|
+
nucliadb_driver,
|
|
120
|
+
source,
|
|
121
|
+
config=self.config,
|
|
122
|
+
question=question,
|
|
123
|
+
step_title=self.step_title("Choose parameters"),
|
|
124
|
+
)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.exception("Error analyzing question")
|
|
127
|
+
raise e
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
if analysis.knowledge_graph is not None:
|
|
131
|
+
search_result = await knowledge_scan(
|
|
132
|
+
memory,
|
|
133
|
+
manager,
|
|
134
|
+
analysis,
|
|
135
|
+
nucliadb_driver,
|
|
136
|
+
config=self.config,
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
search_result = await query_ndb(
|
|
140
|
+
memory,
|
|
141
|
+
manager,
|
|
142
|
+
analysis,
|
|
143
|
+
nucliadb_driver,
|
|
144
|
+
config=self.config,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.exception("Error querying NDB")
|
|
149
|
+
raise e
|
|
150
|
+
else:
|
|
151
|
+
search_result = await standard_query_ndb(
|
|
152
|
+
memory,
|
|
153
|
+
manager,
|
|
154
|
+
nucliadb_driver,
|
|
155
|
+
config=self.config,
|
|
156
|
+
question=question,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
await hydrate(
|
|
160
|
+
context,
|
|
161
|
+
search_result,
|
|
162
|
+
nucliadb_driver=nucliadb_driver,
|
|
163
|
+
vllm=self.config.vllm,
|
|
164
|
+
after=self.config.after,
|
|
165
|
+
before=self.config.before,
|
|
166
|
+
full_resource=self.config.full_resource,
|
|
167
|
+
visual=analysis.visual if analysis is not None else False,
|
|
168
|
+
link=analysis.link if analysis is not None else False,
|
|
169
|
+
module=self.config.module,
|
|
170
|
+
)
|
|
171
|
+
await rerank(context, manager=manager, top_k=analysis.top_k if analysis else 20)
|
|
172
|
+
|
|
173
|
+
async def get_source(
|
|
174
|
+
self, question: str, memory: QuestionMemory, manager: Manager
|
|
175
|
+
) -> List[Source]:
|
|
176
|
+
if len(self.config.sources) == 0:
|
|
177
|
+
# Do we really want to do this without telling the user?
|
|
178
|
+
sources = manager.nucliadbs()
|
|
179
|
+
else:
|
|
180
|
+
sources = self.config.sources
|
|
181
|
+
|
|
182
|
+
# Only makes sense if we have more than one source
|
|
183
|
+
chosen_sources = await choose_source(
|
|
184
|
+
memory,
|
|
185
|
+
manager,
|
|
186
|
+
sources,
|
|
187
|
+
question,
|
|
188
|
+
ident=self.config.id if self.config.id else "default",
|
|
189
|
+
step_title=self.step_title("Choose sources"),
|
|
190
|
+
)
|
|
191
|
+
return chosen_sources
|
|
192
|
+
|
|
193
|
+
@trace_agent
|
|
194
|
+
async def get_question_context(
|
|
195
|
+
self,
|
|
196
|
+
memory: QuestionMemory,
|
|
197
|
+
manager: Manager,
|
|
198
|
+
question_uuid: str,
|
|
199
|
+
question: str,
|
|
200
|
+
flow_id: str,
|
|
201
|
+
extra_context: Optional[Dict[str, Any]] = None,
|
|
202
|
+
):
|
|
203
|
+
if len(self.config.sources) == 0:
|
|
204
|
+
# Do we really want to do this without telling the user?
|
|
205
|
+
sources = manager.nucliadbs()
|
|
206
|
+
else:
|
|
207
|
+
sources = self.config.sources
|
|
208
|
+
|
|
209
|
+
# Only makes sense if we have more than one source
|
|
210
|
+
chosen_sources = await choose_source(
|
|
211
|
+
memory,
|
|
212
|
+
manager,
|
|
213
|
+
sources,
|
|
214
|
+
question,
|
|
215
|
+
ident=self.config.id if self.config.id else "default",
|
|
216
|
+
step_title=self.step_title("Choose sources"),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
await asyncio.gather(
|
|
220
|
+
*[
|
|
221
|
+
self.rag(
|
|
222
|
+
memory, manager, source, question_uuid, question, flow_id=flow_id
|
|
223
|
+
)
|
|
224
|
+
for source in chosen_sources
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
async def rag(
|
|
229
|
+
self,
|
|
230
|
+
memory: QuestionMemory,
|
|
231
|
+
manager: Manager,
|
|
232
|
+
source_obj: Source,
|
|
233
|
+
question_uuid: str,
|
|
234
|
+
question: str,
|
|
235
|
+
flow_id: str,
|
|
236
|
+
):
|
|
237
|
+
source = source_obj.id
|
|
238
|
+
nucliadb_driver: Optional[NucliaDBDriver] = cast(
|
|
239
|
+
NucliaDBDriver, manager.drivers.get(source)
|
|
240
|
+
)
|
|
241
|
+
if nucliadb_driver is None:
|
|
242
|
+
raise Exception("No NDB available")
|
|
243
|
+
|
|
244
|
+
context = Context(
|
|
245
|
+
agent_id=self.config.id,
|
|
246
|
+
original_question_uuid=memory.original_question_uuid,
|
|
247
|
+
actual_question_uuid=question_uuid,
|
|
248
|
+
question=question,
|
|
249
|
+
source=source,
|
|
250
|
+
agent="ask",
|
|
251
|
+
title=self.config.title
|
|
252
|
+
if self.config.title
|
|
253
|
+
else f"Retrieval on {source} Knowledge Box",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
await self.work(memory, manager, context, nucliadb_driver, source_obj, question)
|
|
257
|
+
ident = self.config.id if self.config.id else "default"
|
|
258
|
+
# If no chunks were found, we can skip the summarization
|
|
259
|
+
if context.chunks is not None and len(context.chunks) != 0:
|
|
260
|
+
# These missing questions are not used as of now
|
|
261
|
+
missing_question = await self.save_ctx_and_return_missing(
|
|
262
|
+
context=context,
|
|
263
|
+
question=question,
|
|
264
|
+
memory=memory,
|
|
265
|
+
manager=manager,
|
|
266
|
+
flow_id=flow_id,
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
missing_question = (question_uuid, question)
|
|
270
|
+
await memory.add_step(
|
|
271
|
+
step_module=self.config.module,
|
|
272
|
+
step_title=self.step_title("NucliaDB context"),
|
|
273
|
+
step_reason="No context found",
|
|
274
|
+
step_value="Unsuccessful retrieval",
|
|
275
|
+
timeit=time() - time(),
|
|
276
|
+
step_agent_path=f"/context/{ident}",
|
|
277
|
+
)
|
|
278
|
+
# Query NDB
|
|
279
|
+
if missing_question is not None:
|
|
280
|
+
missing_uuid, missing = missing_question
|
|
281
|
+
# This is always failing back to another nucliadb call in case of missing information
|
|
282
|
+
# TODO: decide if we want to do this or not
|
|
283
|
+
# implicit fallback to another NDB call may introduce unnecessary delay
|
|
284
|
+
# for now I'm only adding it if we have no fallback agent configured
|
|
285
|
+
if self.config.fallback is None:
|
|
286
|
+
context = Context(
|
|
287
|
+
agent_id=self.config.id,
|
|
288
|
+
original_question_uuid=memory.original_question_uuid,
|
|
289
|
+
actual_question_uuid=missing_uuid,
|
|
290
|
+
question=missing,
|
|
291
|
+
source=source,
|
|
292
|
+
agent="ask",
|
|
293
|
+
title=self.config.title
|
|
294
|
+
if self.config.title
|
|
295
|
+
else f"Retrieval on {source} Knowledge Box",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
await self.work(
|
|
299
|
+
memory,
|
|
300
|
+
manager,
|
|
301
|
+
context,
|
|
302
|
+
nucliadb_driver,
|
|
303
|
+
source_obj,
|
|
304
|
+
question,
|
|
305
|
+
)
|
|
306
|
+
new_missing_questions = await self.save_ctx_and_return_missing(
|
|
307
|
+
context=context,
|
|
308
|
+
question=missing,
|
|
309
|
+
memory=memory,
|
|
310
|
+
manager=manager,
|
|
311
|
+
flow_id=flow_id,
|
|
312
|
+
)
|
|
313
|
+
if new_missing_questions is not None:
|
|
314
|
+
# TODO: add proper structure to notify the user that the answer is missing
|
|
315
|
+
logger.warning(
|
|
316
|
+
f"Missing information after fallback: {[m[1] for m in new_missing_questions]}. No fallback agent configured."
|
|
317
|
+
)
|
|
318
|
+
elif self.fallback is not None:
|
|
319
|
+
# TODO: Add next agent
|
|
320
|
+
await self.fallback.get_question_context(
|
|
321
|
+
memory,
|
|
322
|
+
manager,
|
|
323
|
+
question=missing,
|
|
324
|
+
question_uuid=missing_uuid,
|
|
325
|
+
flow_id=flow_id,
|
|
326
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import List, Literal, Optional
|
|
2
|
+
|
|
3
|
+
from hyperforge.context.config import ContextAgentConfig
|
|
4
|
+
from hyperforge.utils import WidgetType
|
|
5
|
+
from nucliadb_models.search import KnowledgeGraphEntity
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from pydantic.config import ConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AskAgentConfig(ContextAgentConfig):
|
|
11
|
+
model_config = ConfigDict(title="Knowledge Box Ask")
|
|
12
|
+
module: Literal["ask"] = "ask"
|
|
13
|
+
published_functions: Optional[tuple[str, ...]] = Field(
|
|
14
|
+
default=("search_by_title", "ask_analysis_query", "ask_agent"),
|
|
15
|
+
title="Published functions",
|
|
16
|
+
description="List of functions published by this agent to be used by other agents in the chain",
|
|
17
|
+
json_schema_extra={
|
|
18
|
+
"widget": WidgetType.NOT_SHOWN,
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
pre_queries: List[str] = Field(default_factory=list)
|
|
22
|
+
filters: List[str] = Field(default_factory=list)
|
|
23
|
+
security_groups: List[str] = Field(default_factory=list)
|
|
24
|
+
rephrase_semantic_custom_prompt: Optional[str] = None
|
|
25
|
+
rephrase_lexical_custom_prompt: Optional[str] = None
|
|
26
|
+
keywords_custom_prompt: Optional[str] = None
|
|
27
|
+
visual_enable_prompt: Optional[str] = None
|
|
28
|
+
date_range_enabled: bool = False
|
|
29
|
+
before: int = 2
|
|
30
|
+
after: int = 2
|
|
31
|
+
top_k: int = 20
|
|
32
|
+
extra_fields: List[str] = Field(default_factory=list)
|
|
33
|
+
full_resource: bool = False
|
|
34
|
+
vllm: bool = True
|
|
35
|
+
query_entities: List[KnowledgeGraphEntity] = Field(default_factory=list)
|
|
36
|
+
retrieve_related: Optional[str] = None
|
|
37
|
+
sources: List[str] = Field(
|
|
38
|
+
default_factory=list,
|
|
39
|
+
json_schema_extra={
|
|
40
|
+
"show_in_node": True,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
configuration_model: str = Field(
|
|
44
|
+
default="gemini-2.5-flash",
|
|
45
|
+
title="Generative model",
|
|
46
|
+
description="Model used to generate the configuration",
|
|
47
|
+
json_schema_extra={"widget": WidgetType.MODEL_SELECT},
|
|
48
|
+
)
|
|
49
|
+
fast_answer: bool = True
|
|
50
|
+
# Setting this to True so that this PR will not break the current behavior
|
|
51
|
+
# of the agent
|
|
52
|
+
ai_parameter_search: bool = True
|