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,59 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, List, Optional
|
|
3
|
+
|
|
4
|
+
from hyperforge.models import FieldTypes
|
|
5
|
+
from nuclia.lib.nua_responses import SemanticConfig
|
|
6
|
+
from nuclia_models.worker.triggers import Relation
|
|
7
|
+
from nucliadb_models.graph.responses import (
|
|
8
|
+
GraphNodesSearchResponse,
|
|
9
|
+
GraphRelationsSearchResponse,
|
|
10
|
+
GraphSearchResponse,
|
|
11
|
+
)
|
|
12
|
+
from nucliadb_models.search import Filter, KnowledgeboxFindResults, KnowledgeGraphEntity
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class NDBChunk:
|
|
17
|
+
chunk_id: str
|
|
18
|
+
text: str
|
|
19
|
+
page_with_visual: bool
|
|
20
|
+
reference: Optional[str]
|
|
21
|
+
start: Optional[int]
|
|
22
|
+
end: Optional[int]
|
|
23
|
+
page: Optional[int]
|
|
24
|
+
labels: List[str]
|
|
25
|
+
field: Optional[FieldTypes]
|
|
26
|
+
resource_labels: List[str]
|
|
27
|
+
link: Optional[str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Analysis:
|
|
32
|
+
semantic_query: str
|
|
33
|
+
lexical_query: str
|
|
34
|
+
visual: bool
|
|
35
|
+
keywords_filter: List[str]
|
|
36
|
+
reason: Optional[str]
|
|
37
|
+
input_tokens: Optional[float]
|
|
38
|
+
output_tokens: Optional[float]
|
|
39
|
+
labels: List[Filter]
|
|
40
|
+
semantic_model: Optional[str]
|
|
41
|
+
entities: List[KnowledgeGraphEntity]
|
|
42
|
+
pre_queries: List[str]
|
|
43
|
+
relations: List[Relation]
|
|
44
|
+
new_text_fields: List[str]
|
|
45
|
+
new_json_fields: dict[str, Any]
|
|
46
|
+
semantic_config: SemanticConfig
|
|
47
|
+
top_k: int
|
|
48
|
+
link: bool
|
|
49
|
+
knowledge_graph: Optional[str]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class SearchResults:
|
|
54
|
+
lexical_find_results: Optional[KnowledgeboxFindResults]
|
|
55
|
+
semantic_find_results: Optional[KnowledgeboxFindResults]
|
|
56
|
+
graph_find_results: Optional[KnowledgeboxFindResults]
|
|
57
|
+
graph_results: Optional[GraphSearchResponse]
|
|
58
|
+
nodes_results: Optional[GraphNodesSearchResponse]
|
|
59
|
+
relations_results: Optional[GraphRelationsSearchResponse]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from time import time
|
|
3
|
+
from typing import List, cast
|
|
4
|
+
|
|
5
|
+
from hyperforge.manager import Manager
|
|
6
|
+
from hyperforge.memory.memory import QuestionMemory
|
|
7
|
+
from hyperforge.models import Source
|
|
8
|
+
|
|
9
|
+
from hyperforge import PROMPT_ENVIRONMENT
|
|
10
|
+
from hyperforge_nucliadb.driver import NucliaDBDriver
|
|
11
|
+
from hyperforge_nucliadb.sync.driver import SyncDriver
|
|
12
|
+
|
|
13
|
+
MULTI_KB_PROMPT = """
|
|
14
|
+
|
|
15
|
+
You have multiple Knowledge Boxes (KBs) that you can use as data sources to ask your questions.
|
|
16
|
+
Every KB has an ID, a description, a set of labels, and facets that characterize its contents.
|
|
17
|
+
Your task is to figure out which KB is the most relevant to a given question by applying the important rules specified.
|
|
18
|
+
|
|
19
|
+
Here are the details of each KB:
|
|
20
|
+
|
|
21
|
+
{% for source in sources.values() %}
|
|
22
|
+
{%- set sid = source.id -%}
|
|
23
|
+
{%- set sdescription = source.description -%}
|
|
24
|
+
{%- set slabels = source.labels -%}
|
|
25
|
+
# ID: {{sid}}
|
|
26
|
+
|
|
27
|
+
{{sdescription}}
|
|
28
|
+
|
|
29
|
+
labels: {{slabels}}
|
|
30
|
+
|
|
31
|
+
{% endfor -%}
|
|
32
|
+
|
|
33
|
+
And given the question: {{question}}
|
|
34
|
+
|
|
35
|
+
# Important rules to follow
|
|
36
|
+
|
|
37
|
+
{% for rule in rules %}
|
|
38
|
+
{{rule}}
|
|
39
|
+
{% endfor -%}
|
|
40
|
+
|
|
41
|
+
Now, provide your answer following the answer rules:
|
|
42
|
+
- Which KB is the most relevant and why(short explanation)?
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
MULTI_KB_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(MULTI_KB_PROMPT)
|
|
46
|
+
|
|
47
|
+
MULTI_KB_SCHEMA = {
|
|
48
|
+
"title": "source_choice",
|
|
49
|
+
"description": "Choose which KBs should be used to answer the question of the user and why",
|
|
50
|
+
"parameters": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"sources": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"description": "the ids of the choosen sources",
|
|
56
|
+
"items": {"type": "string"},
|
|
57
|
+
},
|
|
58
|
+
"reason": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "reasoning behind the decision",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def choose_source(
|
|
68
|
+
memory: QuestionMemory,
|
|
69
|
+
manager: Manager,
|
|
70
|
+
sources: List[str],
|
|
71
|
+
question: str,
|
|
72
|
+
ident: str,
|
|
73
|
+
choose_source_model: str = "chatgpt-o3-mini",
|
|
74
|
+
step_title: str = "Knowledge Box Ask: Choose sources",
|
|
75
|
+
) -> list[Source]:
|
|
76
|
+
if len(sources) == 0:
|
|
77
|
+
raise Exception("No sources available")
|
|
78
|
+
|
|
79
|
+
sources_objects = {}
|
|
80
|
+
for source_id in sources:
|
|
81
|
+
source = await memory.get_session_source(source_id)
|
|
82
|
+
if source is None:
|
|
83
|
+
source = await load_source_information(source_id, manager)
|
|
84
|
+
await memory.set_session_source(source)
|
|
85
|
+
sources_objects[source_id] = source
|
|
86
|
+
|
|
87
|
+
# Only one source, try to use it without involving the LLM
|
|
88
|
+
if len(sources) == 1:
|
|
89
|
+
return [sources_objects[sources[0]]]
|
|
90
|
+
|
|
91
|
+
t0 = time()
|
|
92
|
+
prompt = MULTI_KB_PROMPT_TEMPLATE.render(
|
|
93
|
+
sources=sources_objects,
|
|
94
|
+
rules=memory.get_rules(),
|
|
95
|
+
question=question,
|
|
96
|
+
)
|
|
97
|
+
data, input, output = await manager.execute_json(
|
|
98
|
+
prompt=prompt,
|
|
99
|
+
schema=MULTI_KB_SCHEMA,
|
|
100
|
+
user_id="source_models",
|
|
101
|
+
model=choose_source_model,
|
|
102
|
+
tracking=memory.get_tracking_info(),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
chosen_sources = cast(list[str], data.get("sources"))
|
|
106
|
+
reason = data.get("reason")
|
|
107
|
+
|
|
108
|
+
await memory.add_step(
|
|
109
|
+
step_module="router",
|
|
110
|
+
step_title=step_title,
|
|
111
|
+
step_value=str(chosen_sources),
|
|
112
|
+
step_reason=reason,
|
|
113
|
+
timeit=time() - t0,
|
|
114
|
+
input_nuclia_tokens=input,
|
|
115
|
+
output_nuclia_tokens=output,
|
|
116
|
+
step_agent_path=f"/context/{ident if ident else 'default'}",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return [sources_objects[chosen] for chosen in chosen_sources]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def load_source_information(source: str, manager: Manager) -> Source:
|
|
123
|
+
driver = manager.drivers.get(source)
|
|
124
|
+
if not isinstance(driver, NucliaDBDriver) and not isinstance(driver, SyncDriver):
|
|
125
|
+
raise ValueError("Source is not a NucliaDB driver")
|
|
126
|
+
(
|
|
127
|
+
description,
|
|
128
|
+
labels,
|
|
129
|
+
facets_native,
|
|
130
|
+
paragraph_facets,
|
|
131
|
+
learning_configuration,
|
|
132
|
+
) = await asyncio.gather(
|
|
133
|
+
driver.description(),
|
|
134
|
+
driver.labels(),
|
|
135
|
+
driver.facets_native(),
|
|
136
|
+
driver.paragraph_facets(),
|
|
137
|
+
driver.get_learning_configuration(),
|
|
138
|
+
)
|
|
139
|
+
return Source(
|
|
140
|
+
id=source,
|
|
141
|
+
description=description,
|
|
142
|
+
labels=labels,
|
|
143
|
+
facets_native=facets_native,
|
|
144
|
+
paragraph_facets=paragraph_facets,
|
|
145
|
+
learning_configuration=learning_configuration,
|
|
146
|
+
)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from asyncio import gather
|
|
2
|
+
from typing import Awaitable, List, Optional, cast
|
|
3
|
+
|
|
4
|
+
from hyperforge.manager import Manager
|
|
5
|
+
from hyperforge.memory.memory import QuestionMemory
|
|
6
|
+
from nucliadb_models.filters import And, Or
|
|
7
|
+
from nucliadb_models.graph.requests import (
|
|
8
|
+
GraphPathQuery,
|
|
9
|
+
)
|
|
10
|
+
from nucliadb_models.resource import ExtractedDataTypeName
|
|
11
|
+
from nucliadb_models.search import (
|
|
12
|
+
FindOptions,
|
|
13
|
+
FindRequest,
|
|
14
|
+
KnowledgeboxFindResults,
|
|
15
|
+
MinScore,
|
|
16
|
+
RerankerName,
|
|
17
|
+
ResourceProperties,
|
|
18
|
+
)
|
|
19
|
+
from nucliadb_models.security import RequestSecurity
|
|
20
|
+
|
|
21
|
+
from hyperforge_nucliadb.ask.config import AskAgentConfig
|
|
22
|
+
from hyperforge_nucliadb.ask.models import Analysis, SearchResults
|
|
23
|
+
from hyperforge_nucliadb.ask.utils import (
|
|
24
|
+
empty,
|
|
25
|
+
get_nodes,
|
|
26
|
+
get_relations,
|
|
27
|
+
)
|
|
28
|
+
from hyperforge_nucliadb.driver import NucliaDBDriver
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def query_relations(
|
|
32
|
+
memory: QuestionMemory,
|
|
33
|
+
manager: Manager,
|
|
34
|
+
analysis: Analysis,
|
|
35
|
+
nucliadb_driver: NucliaDBDriver,
|
|
36
|
+
config: AskAgentConfig,
|
|
37
|
+
relations: List[GraphPathQuery],
|
|
38
|
+
nodes: List[GraphPathQuery],
|
|
39
|
+
) -> Awaitable[KnowledgeboxFindResults]:
|
|
40
|
+
"""Query relations in NucliaDB based on the provided analysis."""
|
|
41
|
+
find_request = FindRequest(
|
|
42
|
+
features=[FindOptions.GRAPH],
|
|
43
|
+
filters=analysis.labels,
|
|
44
|
+
graph_query=And(
|
|
45
|
+
operands=[
|
|
46
|
+
Or(operands=relations),
|
|
47
|
+
Or(operands=nodes),
|
|
48
|
+
]
|
|
49
|
+
),
|
|
50
|
+
security=RequestSecurity(groups=config.security_groups),
|
|
51
|
+
reranker=RerankerName.NOOP,
|
|
52
|
+
show=[
|
|
53
|
+
ResourceProperties.BASIC,
|
|
54
|
+
ResourceProperties.ORIGIN,
|
|
55
|
+
ResourceProperties.EXTRA,
|
|
56
|
+
ResourceProperties.VALUES,
|
|
57
|
+
ResourceProperties.RELATIONS,
|
|
58
|
+
],
|
|
59
|
+
)
|
|
60
|
+
return nucliadb_driver.find_raw(find_request)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def query_semantic(
|
|
64
|
+
memory: QuestionMemory,
|
|
65
|
+
manager: Manager,
|
|
66
|
+
analysis: Analysis,
|
|
67
|
+
nucliadb_driver: NucliaDBDriver,
|
|
68
|
+
config: AskAgentConfig,
|
|
69
|
+
) -> Awaitable[KnowledgeboxFindResults]:
|
|
70
|
+
"""Semantic search in NucliaDB based on the provided analysis."""
|
|
71
|
+
|
|
72
|
+
find_request = FindRequest(
|
|
73
|
+
features=[FindOptions.SEMANTIC],
|
|
74
|
+
query=analysis.semantic_query,
|
|
75
|
+
min_score=MinScore(semantic=analysis.semantic_config.threshold),
|
|
76
|
+
vectorset=analysis.semantic_model,
|
|
77
|
+
filters=analysis.labels,
|
|
78
|
+
security=RequestSecurity(groups=config.security_groups),
|
|
79
|
+
reranker=RerankerName.NOOP,
|
|
80
|
+
show=[
|
|
81
|
+
ResourceProperties.BASIC,
|
|
82
|
+
ResourceProperties.ORIGIN,
|
|
83
|
+
ResourceProperties.EXTRA,
|
|
84
|
+
ResourceProperties.EXTRACTED,
|
|
85
|
+
ResourceProperties.VALUES,
|
|
86
|
+
ResourceProperties.RELATIONS,
|
|
87
|
+
],
|
|
88
|
+
extracted=[
|
|
89
|
+
ExtractedDataTypeName.TEXT,
|
|
90
|
+
ExtractedDataTypeName.METADATA,
|
|
91
|
+
ExtractedDataTypeName.FILE,
|
|
92
|
+
ExtractedDataTypeName.LINK,
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return nucliadb_driver.find_raw(find_request)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def query_lexical(
|
|
100
|
+
memory: QuestionMemory,
|
|
101
|
+
manager: Manager,
|
|
102
|
+
analysis: Analysis,
|
|
103
|
+
nucliadb_driver: NucliaDBDriver,
|
|
104
|
+
config: AskAgentConfig,
|
|
105
|
+
) -> Awaitable[KnowledgeboxFindResults]:
|
|
106
|
+
"""Lexical search (aka keyword search) in NucliaDB based on the provided analysis."""
|
|
107
|
+
|
|
108
|
+
find_request = FindRequest(
|
|
109
|
+
features=[FindOptions.KEYWORD],
|
|
110
|
+
query=analysis.lexical_query,
|
|
111
|
+
filters=analysis.labels,
|
|
112
|
+
keyword_filters=analysis.keywords_filter,
|
|
113
|
+
security=RequestSecurity(groups=config.security_groups),
|
|
114
|
+
reranker=RerankerName.NOOP,
|
|
115
|
+
show=[
|
|
116
|
+
ResourceProperties.BASIC,
|
|
117
|
+
ResourceProperties.ORIGIN,
|
|
118
|
+
ResourceProperties.EXTRA,
|
|
119
|
+
ResourceProperties.EXTRACTED,
|
|
120
|
+
ResourceProperties.VALUES,
|
|
121
|
+
ResourceProperties.RELATIONS,
|
|
122
|
+
],
|
|
123
|
+
extracted=[
|
|
124
|
+
ExtractedDataTypeName.TEXT,
|
|
125
|
+
ExtractedDataTypeName.METADATA,
|
|
126
|
+
ExtractedDataTypeName.FILE,
|
|
127
|
+
ExtractedDataTypeName.LINK,
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return nucliadb_driver.find_raw(find_request)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def query_ndb(
|
|
135
|
+
memory: QuestionMemory,
|
|
136
|
+
manager: Manager,
|
|
137
|
+
analysis: Analysis,
|
|
138
|
+
nucliadb_driver: NucliaDBDriver,
|
|
139
|
+
config: AskAgentConfig,
|
|
140
|
+
) -> SearchResults:
|
|
141
|
+
semantic_paragraphs = query_semantic(
|
|
142
|
+
memory, manager, analysis, nucliadb_driver, config
|
|
143
|
+
)
|
|
144
|
+
lexical_paragraphs = query_lexical(
|
|
145
|
+
memory, manager, analysis, nucliadb_driver, config
|
|
146
|
+
)
|
|
147
|
+
nodes = cast(list[GraphPathQuery], get_nodes(analysis))
|
|
148
|
+
relations = cast(list[GraphPathQuery], get_relations(analysis))
|
|
149
|
+
|
|
150
|
+
if len(relations) > 0 and len(nodes) > 0:
|
|
151
|
+
relations_paragraphs: Awaitable[Optional[KnowledgeboxFindResults]] = (
|
|
152
|
+
query_relations(
|
|
153
|
+
memory,
|
|
154
|
+
manager,
|
|
155
|
+
analysis,
|
|
156
|
+
nucliadb_driver,
|
|
157
|
+
config,
|
|
158
|
+
relations=cast(list[GraphPathQuery], relations),
|
|
159
|
+
nodes=cast(list[GraphPathQuery], nodes),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
else:
|
|
164
|
+
relations_paragraphs = empty()
|
|
165
|
+
|
|
166
|
+
(
|
|
167
|
+
lexical_find_results,
|
|
168
|
+
semantic_find_results,
|
|
169
|
+
graph_find_results,
|
|
170
|
+
) = await gather(
|
|
171
|
+
lexical_paragraphs,
|
|
172
|
+
semantic_paragraphs,
|
|
173
|
+
relations_paragraphs,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
len(lexical_find_results.resources) == 0
|
|
178
|
+
and analysis.keywords_filter is not None
|
|
179
|
+
and len(analysis.keywords_filter) > 0
|
|
180
|
+
):
|
|
181
|
+
# XXX: This is repeating the lexical query with the same parameters no?
|
|
182
|
+
|
|
183
|
+
# If no lexical results, we can try to find some using the keywords filter
|
|
184
|
+
lexical_find_results = await query_lexical(
|
|
185
|
+
memory, manager, analysis, nucliadb_driver, config
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
result = SearchResults(
|
|
189
|
+
lexical_find_results=lexical_find_results,
|
|
190
|
+
semantic_find_results=semantic_find_results,
|
|
191
|
+
graph_find_results=graph_find_results,
|
|
192
|
+
graph_results=None,
|
|
193
|
+
nodes_results=None,
|
|
194
|
+
relations_results=None,
|
|
195
|
+
)
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def standard_query_ndb(
|
|
200
|
+
memory: QuestionMemory,
|
|
201
|
+
manager: Manager,
|
|
202
|
+
nucliadb_driver: NucliaDBDriver,
|
|
203
|
+
config: AskAgentConfig,
|
|
204
|
+
question: str,
|
|
205
|
+
) -> SearchResults:
|
|
206
|
+
find_request = FindRequest(
|
|
207
|
+
features=[FindOptions.SEMANTIC, FindOptions.KEYWORD],
|
|
208
|
+
query=question,
|
|
209
|
+
security=RequestSecurity(groups=config.security_groups),
|
|
210
|
+
reranker=RerankerName.NOOP,
|
|
211
|
+
show=[
|
|
212
|
+
ResourceProperties.BASIC,
|
|
213
|
+
ResourceProperties.ORIGIN,
|
|
214
|
+
ResourceProperties.EXTRA,
|
|
215
|
+
ResourceProperties.EXTRACTED,
|
|
216
|
+
ResourceProperties.VALUES,
|
|
217
|
+
ResourceProperties.RELATIONS,
|
|
218
|
+
],
|
|
219
|
+
extracted=[
|
|
220
|
+
ExtractedDataTypeName.TEXT,
|
|
221
|
+
ExtractedDataTypeName.METADATA,
|
|
222
|
+
ExtractedDataTypeName.FILE,
|
|
223
|
+
ExtractedDataTypeName.LINK,
|
|
224
|
+
],
|
|
225
|
+
filters=nucliadb_driver.config.filters,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
paragraphs = await nucliadb_driver.find_raw(find_request)
|
|
229
|
+
|
|
230
|
+
result = SearchResults(
|
|
231
|
+
lexical_find_results=None,
|
|
232
|
+
semantic_find_results=paragraphs,
|
|
233
|
+
graph_find_results=None,
|
|
234
|
+
graph_results=None,
|
|
235
|
+
nodes_results=None,
|
|
236
|
+
relations_results=None,
|
|
237
|
+
)
|
|
238
|
+
return result
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
from hyperforge_nucliadb.ask.config import AskAgentConfig
|
|
4
|
+
from hyperforge_nucliadb.ask.kb_analysis import KnowledgeBoxInfo
|
|
5
|
+
from hyperforge_nucliadb.ask.query_analysis import QueryAnalisys
|
|
6
|
+
|
|
7
|
+
# https://huggingface.co/collections/ModelSpace/gemmax2-673714f5049bfa3a90bee6b6
|
|
8
|
+
TRANSLATE_QUERY_PROMPT = "Rephrase this question so its better for lexical retrieval, translate the lexical rephrased question to {language}. Please define ONLY the question without any explanation. JUST A SENTENCE. DO NOT ADD ANY EXTRA NOTES AT THE END, JUST ONE SENTENCE"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def configure_prompts(
|
|
12
|
+
config: AskAgentConfig,
|
|
13
|
+
ask_json_schema_copy: Dict[str, Dict[str, Any]],
|
|
14
|
+
kb_analysis: KnowledgeBoxInfo,
|
|
15
|
+
query_analysis: QueryAnalisys,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Configure the prompts based on the provided configuration.
|
|
19
|
+
This function modifies the ask_json_schema_copy in place to set the
|
|
20
|
+
appropriate prompts for semantic, lexical, and visual queries.
|
|
21
|
+
"""
|
|
22
|
+
if config.visual_enable_prompt is not None:
|
|
23
|
+
ask_json_schema_copy["parameters"]["properties"]["visual"]["description"] = (
|
|
24
|
+
config.visual_enable_prompt
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if config.rephrase_semantic_custom_prompt is not None:
|
|
28
|
+
ask_json_schema_copy["parameters"]["properties"]["semantic_query"][
|
|
29
|
+
"description"
|
|
30
|
+
] = config.rephrase_semantic_custom_prompt
|
|
31
|
+
|
|
32
|
+
# Rephrase as lexical to query lexical engine
|
|
33
|
+
if config.rephrase_lexical_custom_prompt is not None:
|
|
34
|
+
ask_json_schema_copy["parameters"]["properties"]["lexical_query"][
|
|
35
|
+
"description"
|
|
36
|
+
] = config.rephrase_lexical_custom_prompt
|
|
37
|
+
elif query_analysis.translate_need is not None:
|
|
38
|
+
ask_json_schema_copy["parameters"]["properties"]["lexical_query"][
|
|
39
|
+
"description"
|
|
40
|
+
] = TRANSLATE_QUERY_PROMPT.format(language=query_analysis.translate_need)
|
|
41
|
+
|
|
42
|
+
# Rephrase as keywords that must appear only if the language of the query matches the language of the KB
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
query_analysis.translate_need is None
|
|
46
|
+
and config.keywords_custom_prompt is not None
|
|
47
|
+
):
|
|
48
|
+
ask_json_schema_copy["parameters"]["properties"]["keywords_filter"][
|
|
49
|
+
"description"
|
|
50
|
+
] = config.keywords_custom_prompt
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from lingua import LanguageDetectorBuilder
|
|
6
|
+
|
|
7
|
+
from hyperforge import logger
|
|
8
|
+
from hyperforge_nucliadb.ask.config import AskAgentConfig
|
|
9
|
+
from hyperforge_nucliadb.ask.kb_analysis import KnowledgeBoxInfo
|
|
10
|
+
|
|
11
|
+
LID_MODEL = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_lingua() -> None:
|
|
15
|
+
global LID_MODEL
|
|
16
|
+
LID_MODEL = (
|
|
17
|
+
LanguageDetectorBuilder.from_all_languages()
|
|
18
|
+
.with_preloaded_language_models()
|
|
19
|
+
.build()
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def detect_language(text: str, default="en") -> str:
|
|
24
|
+
global LID_MODEL
|
|
25
|
+
lang = default
|
|
26
|
+
|
|
27
|
+
if len(text.strip()) == 0:
|
|
28
|
+
return default
|
|
29
|
+
|
|
30
|
+
if isinstance(text, bytes):
|
|
31
|
+
text = text.decode()
|
|
32
|
+
|
|
33
|
+
text = cleanup_text_for_langdetect(text)
|
|
34
|
+
|
|
35
|
+
if LID_MODEL is None:
|
|
36
|
+
load_lingua()
|
|
37
|
+
assert LID_MODEL is not None # for type checker
|
|
38
|
+
result = LID_MODEL.detect_language_of(text.replace("\n", " "))
|
|
39
|
+
|
|
40
|
+
if result is not None:
|
|
41
|
+
lang = result.iso_code_639_1.name.lower()
|
|
42
|
+
|
|
43
|
+
return lang
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cleanup_text_for_langdetect(text: str) -> str:
|
|
47
|
+
# Get all text from splitting docs
|
|
48
|
+
regex = re.compile(r"(\[image: [a-zA-Z0-9.]*\])", re.S)
|
|
49
|
+
return re.sub(regex, "", text)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class QueryAnalisys:
|
|
54
|
+
translate_need: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pre_query_analysis(
|
|
58
|
+
config: AskAgentConfig, question: str, kb_analysis: KnowledgeBoxInfo
|
|
59
|
+
) -> QueryAnalisys:
|
|
60
|
+
# LLM QUERY
|
|
61
|
+
result = QueryAnalisys()
|
|
62
|
+
|
|
63
|
+
# Rephrase as semantic to query semantic engine -> custom prompt
|
|
64
|
+
|
|
65
|
+
query_language = detect_language(question)
|
|
66
|
+
|
|
67
|
+
# Check if the query language is in the languages of the KB
|
|
68
|
+
# or get the most common language in the KB to translate the query
|
|
69
|
+
if query_language in kb_analysis.languages:
|
|
70
|
+
result.translate_need = None
|
|
71
|
+
else:
|
|
72
|
+
try:
|
|
73
|
+
result.translate_need = sorted(
|
|
74
|
+
kb_analysis.languages.items(), key=lambda item: item[1]
|
|
75
|
+
)[0][0]
|
|
76
|
+
except IndexError:
|
|
77
|
+
logger.warning(
|
|
78
|
+
"No languages found in the KB analysis, cannot determine if translation is needed"
|
|
79
|
+
)
|
|
80
|
+
# Kb may not have any languages
|
|
81
|
+
result.translate_need = None
|
|
82
|
+
|
|
83
|
+
return result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from hyperforge.manager import Manager
|
|
2
|
+
from hyperforge.models import Context
|
|
3
|
+
from nuclia.lib.nua_responses import RerankModel
|
|
4
|
+
|
|
5
|
+
from hyperforge import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def rerank(context: Context, manager: Manager, top_k: int) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Rerank the context chunks based on the analysis.
|
|
11
|
+
"""
|
|
12
|
+
if not context.chunks:
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
re_ranked = await manager.rerank(
|
|
16
|
+
RerankModel(
|
|
17
|
+
context={chunk.chunk_id: chunk.text for chunk in context.chunks},
|
|
18
|
+
question=context.question,
|
|
19
|
+
user_id="arag-ask-rerank",
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Sort chunks by score
|
|
24
|
+
re_ranked_sorted = sorted(
|
|
25
|
+
re_ranked.context_scores.items(), key=lambda x: x[1], reverse=True
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
re_ranked_sorted = list(
|
|
29
|
+
filter(
|
|
30
|
+
lambda x: x[1] > 0.05,
|
|
31
|
+
re_ranked.context_scores.items(),
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
re_ranked_sorted = re_ranked_sorted[:top_k]
|
|
36
|
+
|
|
37
|
+
# If the analysis has a rerank function, use it to rerank the chunks
|
|
38
|
+
old_chunks = {chunk.chunk_id: chunk for chunk in context.chunks}
|
|
39
|
+
context.chunks = []
|
|
40
|
+
for rerank_chunk_id, _ in re_ranked_sorted:
|
|
41
|
+
try:
|
|
42
|
+
chunk = old_chunks[rerank_chunk_id]
|
|
43
|
+
context.chunks.append(chunk)
|
|
44
|
+
except KeyError:
|
|
45
|
+
logger.error(f"Chunk {rerank_chunk_id} not found in old_chunks")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from nucliadb_models.graph.requests import (
|
|
5
|
+
AnyNode,
|
|
6
|
+
NodeMatchKindName,
|
|
7
|
+
Relation,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from hyperforge_nucliadb.ask.models import Analysis
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def empty() -> None:
|
|
14
|
+
"""A placeholder to call."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_nodes(analysis: Analysis) -> List[AnyNode]:
|
|
19
|
+
"""Extract nodes from the analysis."""
|
|
20
|
+
nodes: List[AnyNode] = []
|
|
21
|
+
for entity in analysis.entities:
|
|
22
|
+
if entity.type == "PERSON":
|
|
23
|
+
nodes.append(AnyNode(value=entity.name, match=NodeMatchKindName.FUZZY))
|
|
24
|
+
elif entity.type == "LOCATION" or entity.type == "ORGANIZATION":
|
|
25
|
+
nodes.append(AnyNode(value=entity.name, match=NodeMatchKindName.EXACT))
|
|
26
|
+
else:
|
|
27
|
+
nodes.append(AnyNode(value=entity.name, match=NodeMatchKindName.FUZZY))
|
|
28
|
+
return nodes
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_relations(analysis: Analysis) -> Sequence[Relation]:
|
|
32
|
+
"""Extract relations from the analysis."""
|
|
33
|
+
relations: List[Relation] = []
|
|
34
|
+
for relation in analysis.relations:
|
|
35
|
+
relations.append(Relation(label=relation.label))
|
|
36
|
+
return relations
|