gnosisllm-knowledge 0.2.0__py3-none-any.whl → 0.4.0__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.
- gnosisllm_knowledge/__init__.py +91 -39
- gnosisllm_knowledge/api/__init__.py +3 -2
- gnosisllm_knowledge/api/knowledge.py +502 -32
- gnosisllm_knowledge/api/memory.py +966 -0
- gnosisllm_knowledge/backends/__init__.py +14 -5
- gnosisllm_knowledge/backends/memory/indexer.py +27 -2
- gnosisllm_knowledge/backends/memory/searcher.py +111 -10
- gnosisllm_knowledge/backends/opensearch/agentic.py +355 -48
- gnosisllm_knowledge/backends/opensearch/config.py +49 -28
- gnosisllm_knowledge/backends/opensearch/indexer.py +49 -3
- gnosisllm_knowledge/backends/opensearch/mappings.py +14 -5
- gnosisllm_knowledge/backends/opensearch/memory/__init__.py +12 -0
- gnosisllm_knowledge/backends/opensearch/memory/client.py +1380 -0
- gnosisllm_knowledge/backends/opensearch/memory/config.py +127 -0
- gnosisllm_knowledge/backends/opensearch/memory/setup.py +322 -0
- gnosisllm_knowledge/backends/opensearch/queries.py +33 -33
- gnosisllm_knowledge/backends/opensearch/searcher.py +238 -0
- gnosisllm_knowledge/backends/opensearch/setup.py +308 -148
- gnosisllm_knowledge/cli/app.py +436 -31
- gnosisllm_knowledge/cli/commands/agentic.py +26 -9
- gnosisllm_knowledge/cli/commands/load.py +169 -19
- gnosisllm_knowledge/cli/commands/memory.py +733 -0
- gnosisllm_knowledge/cli/commands/search.py +9 -10
- gnosisllm_knowledge/cli/commands/setup.py +49 -23
- gnosisllm_knowledge/cli/display/service.py +43 -0
- gnosisllm_knowledge/cli/utils/config.py +62 -4
- gnosisllm_knowledge/core/domain/__init__.py +54 -0
- gnosisllm_knowledge/core/domain/discovery.py +166 -0
- gnosisllm_knowledge/core/domain/document.py +19 -19
- gnosisllm_knowledge/core/domain/memory.py +440 -0
- gnosisllm_knowledge/core/domain/result.py +11 -3
- gnosisllm_knowledge/core/domain/search.py +12 -25
- gnosisllm_knowledge/core/domain/source.py +11 -12
- gnosisllm_knowledge/core/events/__init__.py +8 -0
- gnosisllm_knowledge/core/events/types.py +198 -5
- gnosisllm_knowledge/core/exceptions.py +227 -0
- gnosisllm_knowledge/core/interfaces/__init__.py +17 -0
- gnosisllm_knowledge/core/interfaces/agentic.py +11 -3
- gnosisllm_knowledge/core/interfaces/indexer.py +10 -1
- gnosisllm_knowledge/core/interfaces/memory.py +524 -0
- gnosisllm_knowledge/core/interfaces/searcher.py +10 -1
- gnosisllm_knowledge/core/interfaces/streaming.py +133 -0
- gnosisllm_knowledge/core/streaming/__init__.py +36 -0
- gnosisllm_knowledge/core/streaming/pipeline.py +228 -0
- gnosisllm_knowledge/fetchers/__init__.py +8 -0
- gnosisllm_knowledge/fetchers/config.py +27 -0
- gnosisllm_knowledge/fetchers/neoreader.py +31 -3
- gnosisllm_knowledge/fetchers/neoreader_discovery.py +505 -0
- gnosisllm_knowledge/loaders/__init__.py +5 -1
- gnosisllm_knowledge/loaders/base.py +3 -4
- gnosisllm_knowledge/loaders/discovery.py +338 -0
- gnosisllm_knowledge/loaders/discovery_streaming.py +343 -0
- gnosisllm_knowledge/loaders/factory.py +46 -0
- gnosisllm_knowledge/loaders/sitemap.py +129 -1
- gnosisllm_knowledge/loaders/sitemap_streaming.py +258 -0
- gnosisllm_knowledge/services/indexing.py +100 -93
- gnosisllm_knowledge/services/search.py +84 -31
- gnosisllm_knowledge/services/streaming_pipeline.py +334 -0
- {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.4.0.dist-info}/METADATA +73 -10
- gnosisllm_knowledge-0.4.0.dist-info/RECORD +81 -0
- gnosisllm_knowledge-0.2.0.dist-info/RECORD +0 -64
- {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.4.0.dist-info}/WHEEL +0 -0
- {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Uses OpenSearch ML agents for AI-powered search with reasoning capabilities.
|
|
4
4
|
Supports flow agents (fast RAG) and conversational agents (multi-turn with memory).
|
|
5
|
+
|
|
6
|
+
Note:
|
|
7
|
+
This module is **tenant-agnostic**. Multi-tenancy is achieved through index isolation:
|
|
8
|
+
each tenant's data resides in a separate OpenSearch index. The caller (e.g., gnosisllm-api)
|
|
9
|
+
is responsible for constructing the appropriate index name (e.g., `knowledge-{account_id}`).
|
|
10
|
+
The library operates on the provided index without any tenant-specific filtering logic.
|
|
5
11
|
"""
|
|
6
12
|
|
|
7
13
|
from __future__ import annotations
|
|
@@ -9,7 +15,6 @@ from __future__ import annotations
|
|
|
9
15
|
import asyncio
|
|
10
16
|
import json
|
|
11
17
|
import logging
|
|
12
|
-
import uuid
|
|
13
18
|
from datetime import UTC, datetime
|
|
14
19
|
from typing import TYPE_CHECKING, Any
|
|
15
20
|
|
|
@@ -96,11 +101,12 @@ class OpenSearchAgenticSearcher:
|
|
|
96
101
|
) -> AgenticSearchResult:
|
|
97
102
|
"""Execute agentic search with agent orchestration.
|
|
98
103
|
|
|
99
|
-
The flow:
|
|
104
|
+
The flow with RAGTool:
|
|
100
105
|
1. Select agent based on query.agent_type
|
|
101
106
|
2. Build execution request with query and filters
|
|
102
107
|
3. Execute agent via OpenSearch ML API
|
|
103
|
-
4.
|
|
108
|
+
4. RAGTool searches the index and generates an AI answer
|
|
109
|
+
5. Parse response for answer, reasoning, and source documents
|
|
104
110
|
|
|
105
111
|
Args:
|
|
106
112
|
query: Agentic search query with agent type and context.
|
|
@@ -121,7 +127,7 @@ class OpenSearchAgenticSearcher:
|
|
|
121
127
|
raise AgenticSearchError(
|
|
122
128
|
message=f"Agent not configured for type: {query.agent_type.value}",
|
|
123
129
|
agent_type=query.agent_type.value,
|
|
124
|
-
details={"hint": "Run 'gnosisllm-knowledge agentic setup' to configure agents."},
|
|
130
|
+
details={"hint": "Run 'gnosisllm-knowledge agentic setup --force' to configure agents."},
|
|
125
131
|
)
|
|
126
132
|
|
|
127
133
|
# Build execution request
|
|
@@ -133,15 +139,114 @@ class OpenSearchAgenticSearcher:
|
|
|
133
139
|
"agent_id": agent_id,
|
|
134
140
|
"agent_type": query.agent_type.value,
|
|
135
141
|
"query": query.text[:100],
|
|
142
|
+
"index_name": index_name,
|
|
136
143
|
},
|
|
137
144
|
)
|
|
138
145
|
|
|
139
|
-
# Execute agent
|
|
140
|
-
|
|
146
|
+
# Execute agent - RAGTool handles search AND answer generation
|
|
147
|
+
agent_response = await self._execute_agent(agent_id, execute_body)
|
|
141
148
|
|
|
142
149
|
duration_ms = (datetime.now(UTC) - start).total_seconds() * 1000
|
|
143
150
|
|
|
144
|
-
return self.
|
|
151
|
+
return self._parse_rag_response(query, agent_response, duration_ms)
|
|
152
|
+
|
|
153
|
+
def _parse_rag_response(
|
|
154
|
+
self,
|
|
155
|
+
query: AgenticSearchQuery,
|
|
156
|
+
response: dict[str, Any],
|
|
157
|
+
duration_ms: float,
|
|
158
|
+
) -> AgenticSearchResult:
|
|
159
|
+
"""Parse RAGTool response into AgenticSearchResult.
|
|
160
|
+
|
|
161
|
+
RAGTool returns both an AI-generated answer and source documents.
|
|
162
|
+
|
|
163
|
+
Response format:
|
|
164
|
+
{
|
|
165
|
+
"inference_results": [
|
|
166
|
+
{
|
|
167
|
+
"output": [
|
|
168
|
+
{"name": "knowledge_search", "result": "<LLM answer>"}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
query: The original query.
|
|
176
|
+
response: Agent execution response.
|
|
177
|
+
duration_ms: Total execution duration.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Parsed AgenticSearchResult with answer and sources.
|
|
181
|
+
"""
|
|
182
|
+
answer: str | None = None
|
|
183
|
+
reasoning_steps: list[ReasoningStep] = []
|
|
184
|
+
items: list[SearchResultItem] = []
|
|
185
|
+
conversation_id = response.get("memory_id")
|
|
186
|
+
|
|
187
|
+
# Parse inference results
|
|
188
|
+
inference_results = response.get("inference_results", [])
|
|
189
|
+
if inference_results:
|
|
190
|
+
outputs = inference_results[0].get("output", [])
|
|
191
|
+
|
|
192
|
+
for output in outputs:
|
|
193
|
+
name = output.get("name", "")
|
|
194
|
+
result = output.get("result", "")
|
|
195
|
+
|
|
196
|
+
# Handle dataAsMap structure
|
|
197
|
+
data_as_map = output.get("dataAsMap", {})
|
|
198
|
+
if data_as_map and "response" in data_as_map:
|
|
199
|
+
result = data_as_map.get("response", result)
|
|
200
|
+
|
|
201
|
+
if name == "memory_id":
|
|
202
|
+
conversation_id = str(result) if result else None
|
|
203
|
+
elif name in ("knowledge_search", "RAGTool", "response"):
|
|
204
|
+
# RAGTool returns the LLM-generated answer
|
|
205
|
+
answer = self._extract_answer_from_result(result)
|
|
206
|
+
|
|
207
|
+
if query.include_reasoning:
|
|
208
|
+
reasoning_steps.append(
|
|
209
|
+
ReasoningStep(
|
|
210
|
+
tool="RAGTool",
|
|
211
|
+
action="rag_search",
|
|
212
|
+
input=query.text,
|
|
213
|
+
output=answer[:200] if answer else None,
|
|
214
|
+
duration_ms=duration_ms,
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Extract source documents if available in the response
|
|
219
|
+
for output in outputs:
|
|
220
|
+
name = output.get("name", "")
|
|
221
|
+
result = output.get("result", "")
|
|
222
|
+
|
|
223
|
+
# Try to extract source documents from additional_info or similar
|
|
224
|
+
additional_info = output.get("additional_info", {})
|
|
225
|
+
if additional_info:
|
|
226
|
+
hits = additional_info.get("hits", {})
|
|
227
|
+
if hits:
|
|
228
|
+
items.extend(self._parse_opensearch_hits(hits))
|
|
229
|
+
|
|
230
|
+
# If no answer from structured output, try raw response
|
|
231
|
+
if not answer and "response" in response:
|
|
232
|
+
answer = response.get("response")
|
|
233
|
+
|
|
234
|
+
# Preserve the query's conversation_id if agent didn't return one
|
|
235
|
+
final_conversation_id = conversation_id or query.conversation_id
|
|
236
|
+
|
|
237
|
+
return AgenticSearchResult(
|
|
238
|
+
query=query.text,
|
|
239
|
+
mode=SearchMode.AGENTIC,
|
|
240
|
+
items=items,
|
|
241
|
+
total_hits=len(items),
|
|
242
|
+
duration_ms=duration_ms,
|
|
243
|
+
max_score=items[0].score if items else None,
|
|
244
|
+
answer=answer,
|
|
245
|
+
reasoning_steps=reasoning_steps,
|
|
246
|
+
conversation_id=final_conversation_id,
|
|
247
|
+
agent_type=query.agent_type,
|
|
248
|
+
citations=[item.doc_id for item in items[:5]],
|
|
249
|
+
)
|
|
145
250
|
|
|
146
251
|
async def get_conversation(
|
|
147
252
|
self,
|
|
@@ -197,13 +302,15 @@ class OpenSearchAgenticSearcher:
|
|
|
197
302
|
|
|
198
303
|
async def list_conversations(
|
|
199
304
|
self,
|
|
200
|
-
account_id: str | None = None,
|
|
201
305
|
limit: int = 100,
|
|
202
306
|
) -> list[dict[str, Any]]:
|
|
203
307
|
"""List active conversations.
|
|
204
308
|
|
|
309
|
+
Note:
|
|
310
|
+
This library is tenant-agnostic. Multi-tenancy is achieved through
|
|
311
|
+
index isolation (separate index per account).
|
|
312
|
+
|
|
205
313
|
Args:
|
|
206
|
-
account_id: Filter by account (multi-tenant).
|
|
207
314
|
limit: Maximum number of conversations.
|
|
208
315
|
|
|
209
316
|
Returns:
|
|
@@ -211,8 +318,6 @@ class OpenSearchAgenticSearcher:
|
|
|
211
318
|
"""
|
|
212
319
|
try:
|
|
213
320
|
body: dict[str, Any] = {"size": limit}
|
|
214
|
-
if account_id:
|
|
215
|
-
body["query"] = {"term": {"account_id": account_id}}
|
|
216
321
|
|
|
217
322
|
response = await self._client.transport.perform_request(
|
|
218
323
|
"POST",
|
|
@@ -265,16 +370,18 @@ class OpenSearchAgenticSearcher:
|
|
|
265
370
|
async def create_conversation(
|
|
266
371
|
self,
|
|
267
372
|
name: str | None = None,
|
|
268
|
-
account_id: str | None = None,
|
|
269
373
|
) -> str | None:
|
|
270
374
|
"""Create a new conversation memory.
|
|
271
375
|
|
|
272
376
|
Uses the OpenSearch Memory API to create a conversation memory.
|
|
273
377
|
The endpoint is POST /_plugins/_ml/memory (introduced in 2.12).
|
|
274
378
|
|
|
379
|
+
Note:
|
|
380
|
+
This library is tenant-agnostic. Multi-tenancy is achieved through
|
|
381
|
+
index isolation (separate index per account).
|
|
382
|
+
|
|
275
383
|
Args:
|
|
276
384
|
name: Optional name for the conversation.
|
|
277
|
-
account_id: Optional account ID for multi-tenancy.
|
|
278
385
|
|
|
279
386
|
Returns:
|
|
280
387
|
The new conversation/memory ID, or None if creation fails.
|
|
@@ -282,8 +389,6 @@ class OpenSearchAgenticSearcher:
|
|
|
282
389
|
body: dict[str, Any] = {}
|
|
283
390
|
if name:
|
|
284
391
|
body["name"] = name
|
|
285
|
-
if account_id:
|
|
286
|
-
body["account_id"] = account_id
|
|
287
392
|
|
|
288
393
|
try:
|
|
289
394
|
# POST /_plugins/_ml/memory creates a new memory (OpenSearch 2.12+)
|
|
@@ -317,17 +422,18 @@ class OpenSearchAgenticSearcher:
|
|
|
317
422
|
) -> dict[str, Any]:
|
|
318
423
|
"""Build agent execution request.
|
|
319
424
|
|
|
320
|
-
|
|
425
|
+
RAGTool requires:
|
|
321
426
|
- question: The user's query (required)
|
|
322
|
-
- memory_id: For conversation continuity (conversational agents)
|
|
323
|
-
- message_history_limit: Number of historical messages to include
|
|
324
427
|
|
|
325
|
-
|
|
326
|
-
|
|
428
|
+
The index is configured at agent creation time, not at execution time.
|
|
429
|
+
RAGTool searches the configured index and generates an AI answer.
|
|
430
|
+
|
|
431
|
+
Conversational agents also support:
|
|
432
|
+
- memory_id: For conversation continuity
|
|
327
433
|
|
|
328
434
|
Args:
|
|
329
435
|
query: The agentic search query.
|
|
330
|
-
index_name: Target index name (not used
|
|
436
|
+
index_name: Target index name (for logging, not used by RAGTool).
|
|
331
437
|
|
|
332
438
|
Returns:
|
|
333
439
|
Request body for agent execution.
|
|
@@ -339,7 +445,6 @@ class OpenSearchAgenticSearcher:
|
|
|
339
445
|
}
|
|
340
446
|
|
|
341
447
|
# Add conversation context for conversational agents
|
|
342
|
-
# OpenSearch handles memory injection automatically with app_type=rag
|
|
343
448
|
if query.agent_type == AgentType.CONVERSATIONAL and query.conversation_id:
|
|
344
449
|
request["parameters"]["memory_id"] = query.conversation_id
|
|
345
450
|
|
|
@@ -386,32 +491,155 @@ class OpenSearchAgenticSearcher:
|
|
|
386
491
|
cause=e,
|
|
387
492
|
)
|
|
388
493
|
|
|
494
|
+
def _extract_dsl_from_agent_response(
|
|
495
|
+
self,
|
|
496
|
+
response: dict[str, Any],
|
|
497
|
+
) -> dict[str, Any] | None:
|
|
498
|
+
"""Extract generated DSL query from agent response.
|
|
499
|
+
|
|
500
|
+
The flow agent with QueryPlanningTool returns the DSL in the output.
|
|
501
|
+
Format: {"inference_results": [{"output": [{"name": "response", "result": "<DSL JSON>"}]}]}
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
response: Agent execution response.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Parsed DSL query dict, or None if not found.
|
|
508
|
+
"""
|
|
509
|
+
try:
|
|
510
|
+
inference_results = response.get("inference_results", [])
|
|
511
|
+
if not inference_results:
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
outputs = inference_results[0].get("output", [])
|
|
515
|
+
for output in outputs:
|
|
516
|
+
name = output.get("name", "")
|
|
517
|
+
result = output.get("result", "")
|
|
518
|
+
|
|
519
|
+
# QueryPlanningTool outputs come as "response" or "query_planner"
|
|
520
|
+
if name in ("response", "query_planner", "QueryPlanningTool"):
|
|
521
|
+
if isinstance(result, dict):
|
|
522
|
+
return result
|
|
523
|
+
if isinstance(result, str) and result.strip():
|
|
524
|
+
# Try to parse as JSON
|
|
525
|
+
return self._parse_dsl_string(result)
|
|
526
|
+
|
|
527
|
+
return None
|
|
528
|
+
except Exception as e:
|
|
529
|
+
self._logger.warning(f"Failed to extract DSL from agent response: {e}")
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
def _parse_dsl_string(self, dsl_string: str) -> dict[str, Any] | None:
|
|
533
|
+
"""Parse a DSL query string into a dictionary.
|
|
534
|
+
|
|
535
|
+
Handles various formats:
|
|
536
|
+
- Raw JSON
|
|
537
|
+
- Markdown code blocks
|
|
538
|
+
- JSON with surrounding text
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
dsl_string: The DSL query as a string.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
Parsed DSL query dict, or None if parsing fails.
|
|
545
|
+
"""
|
|
546
|
+
dsl_string = dsl_string.strip()
|
|
547
|
+
|
|
548
|
+
# Remove markdown code blocks if present
|
|
549
|
+
if dsl_string.startswith("```"):
|
|
550
|
+
lines = dsl_string.split("\n")
|
|
551
|
+
# Remove first line (```json or ```)
|
|
552
|
+
lines = lines[1:] if lines else []
|
|
553
|
+
# Remove last line (```)
|
|
554
|
+
if lines and lines[-1].strip() == "```":
|
|
555
|
+
lines = lines[:-1]
|
|
556
|
+
dsl_string = "\n".join(lines).strip()
|
|
557
|
+
|
|
558
|
+
# Try to find and parse JSON
|
|
559
|
+
try:
|
|
560
|
+
# Find the first { and last }
|
|
561
|
+
start = dsl_string.find("{")
|
|
562
|
+
end = dsl_string.rfind("}") + 1
|
|
563
|
+
if start >= 0 and end > start:
|
|
564
|
+
json_str = dsl_string[start:end]
|
|
565
|
+
return json.loads(json_str)
|
|
566
|
+
except json.JSONDecodeError as e:
|
|
567
|
+
self._logger.debug(f"Failed to parse DSL JSON: {e}")
|
|
568
|
+
|
|
569
|
+
# Try parsing the whole string
|
|
570
|
+
try:
|
|
571
|
+
return json.loads(dsl_string)
|
|
572
|
+
except json.JSONDecodeError:
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
async def _execute_dsl_query(
|
|
578
|
+
self,
|
|
579
|
+
index_name: str,
|
|
580
|
+
dsl_query: dict[str, Any],
|
|
581
|
+
) -> dict[str, Any]:
|
|
582
|
+
"""Execute a DSL query against the index.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
index_name: Target index name.
|
|
586
|
+
dsl_query: OpenSearch DSL query to execute.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Search response with hits.
|
|
590
|
+
|
|
591
|
+
Raises:
|
|
592
|
+
AgenticSearchError: If query execution fails.
|
|
593
|
+
"""
|
|
594
|
+
try:
|
|
595
|
+
response = await self._client.search(
|
|
596
|
+
index=index_name,
|
|
597
|
+
body=dsl_query,
|
|
598
|
+
)
|
|
599
|
+
return response
|
|
600
|
+
except Exception as e:
|
|
601
|
+
self._logger.error(f"DSL query execution failed: {e}")
|
|
602
|
+
raise AgenticSearchError(
|
|
603
|
+
message=f"Failed to execute generated DSL query: {e}",
|
|
604
|
+
details={"index_name": index_name, "query": str(dsl_query)[:200]},
|
|
605
|
+
cause=e,
|
|
606
|
+
)
|
|
607
|
+
|
|
389
608
|
def _parse_agentic_response(
|
|
390
609
|
self,
|
|
391
610
|
query: AgenticSearchQuery,
|
|
392
|
-
|
|
611
|
+
agent_response: dict[str, Any],
|
|
393
612
|
duration_ms: float,
|
|
613
|
+
search_response: dict[str, Any] | None = None,
|
|
614
|
+
generated_dsl: dict[str, Any] | None = None,
|
|
394
615
|
) -> AgenticSearchResult:
|
|
395
616
|
"""Parse agent response into AgenticSearchResult.
|
|
396
617
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
618
|
+
Supports two response formats:
|
|
619
|
+
|
|
620
|
+
1. QueryPlanningTool (OpenSearch 3.2+):
|
|
621
|
+
The agent generates DSL queries which we then execute.
|
|
622
|
+
Agent response: {"inference_results": [{"output": [{"name": "response", "result": "<DSL JSON>"}]}]}
|
|
623
|
+
Search response: Standard OpenSearch search response with hits.
|
|
624
|
+
|
|
625
|
+
2. Legacy VectorDBTool + MLModelTool:
|
|
626
|
+
{
|
|
627
|
+
"inference_results": [
|
|
628
|
+
{
|
|
629
|
+
"output": [
|
|
630
|
+
{"name": "knowledge_search", "result": {...}},
|
|
631
|
+
{"name": "answer_generator", "result": "..."}
|
|
632
|
+
]
|
|
633
|
+
}
|
|
634
|
+
]
|
|
635
|
+
}
|
|
410
636
|
|
|
411
637
|
Args:
|
|
412
638
|
query: The original query.
|
|
413
|
-
|
|
639
|
+
agent_response: Agent execution response.
|
|
414
640
|
duration_ms: Total execution duration.
|
|
641
|
+
search_response: Search results from executing the generated DSL (optional).
|
|
642
|
+
generated_dsl: The DSL query generated by the agent (optional).
|
|
415
643
|
|
|
416
644
|
Returns:
|
|
417
645
|
Parsed AgenticSearchResult.
|
|
@@ -419,13 +647,40 @@ class OpenSearchAgenticSearcher:
|
|
|
419
647
|
answer: str | None = None
|
|
420
648
|
reasoning_steps: list[ReasoningStep] = []
|
|
421
649
|
items: list[SearchResultItem] = []
|
|
422
|
-
conversation_id =
|
|
650
|
+
conversation_id = agent_response.get("memory_id")
|
|
651
|
+
dsl_string: str | None = None
|
|
423
652
|
total_tokens = 0
|
|
424
653
|
prompt_tokens = 0
|
|
425
654
|
completion_tokens = 0
|
|
426
655
|
|
|
427
|
-
# Parse
|
|
428
|
-
|
|
656
|
+
# Parse search results from executed DSL query first (QueryPlanningTool flow)
|
|
657
|
+
if search_response:
|
|
658
|
+
hits_data = search_response.get("hits", {})
|
|
659
|
+
items.extend(self._parse_opensearch_hits(hits_data))
|
|
660
|
+
|
|
661
|
+
if query.include_reasoning:
|
|
662
|
+
dsl_string = json.dumps(generated_dsl) if generated_dsl else None
|
|
663
|
+
reasoning_steps.append(
|
|
664
|
+
ReasoningStep(
|
|
665
|
+
tool="QueryPlanningTool",
|
|
666
|
+
action="query_generation",
|
|
667
|
+
input=query.text,
|
|
668
|
+
output=dsl_string[:200] if dsl_string else None,
|
|
669
|
+
duration_ms=0,
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
reasoning_steps.append(
|
|
673
|
+
ReasoningStep(
|
|
674
|
+
tool="QueryPlanningTool",
|
|
675
|
+
action="search_execution",
|
|
676
|
+
input=dsl_string[:100] if dsl_string else query.text,
|
|
677
|
+
output=f"Found {len(items)} documents",
|
|
678
|
+
duration_ms=0,
|
|
679
|
+
)
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Parse inference results for additional outputs (legacy or conversational)
|
|
683
|
+
inference_results = agent_response.get("inference_results", [])
|
|
429
684
|
if inference_results:
|
|
430
685
|
outputs = inference_results[0].get("output", [])
|
|
431
686
|
|
|
@@ -443,8 +698,8 @@ class OpenSearchAgenticSearcher:
|
|
|
443
698
|
elif name == "parent_message_id":
|
|
444
699
|
# Track parent message ID for conversation threading
|
|
445
700
|
pass # Could store for future use
|
|
446
|
-
elif name in ("
|
|
447
|
-
# Parse answer from output
|
|
701
|
+
elif name in ("answer_generator", "MLModelTool"):
|
|
702
|
+
# Parse answer from output (legacy format)
|
|
448
703
|
answer = self._extract_answer_from_result(result)
|
|
449
704
|
|
|
450
705
|
# Add reasoning step for answer generation
|
|
@@ -459,7 +714,7 @@ class OpenSearchAgenticSearcher:
|
|
|
459
714
|
)
|
|
460
715
|
)
|
|
461
716
|
elif name in ("knowledge_search", "VectorDBTool"):
|
|
462
|
-
# Parse search results from
|
|
717
|
+
# Parse search results from legacy VectorDBTool output
|
|
463
718
|
items.extend(self._parse_tool_search_results(result))
|
|
464
719
|
|
|
465
720
|
# Add reasoning step
|
|
@@ -470,9 +725,10 @@ class OpenSearchAgenticSearcher:
|
|
|
470
725
|
action="search",
|
|
471
726
|
input=query.text,
|
|
472
727
|
output=f"Found {len(items)} documents",
|
|
473
|
-
duration_ms=0,
|
|
728
|
+
duration_ms=0,
|
|
474
729
|
)
|
|
475
730
|
)
|
|
731
|
+
# Skip "response" and "query_planner" here - they're handled via generated_dsl parameter
|
|
476
732
|
|
|
477
733
|
# Parse token usage if available
|
|
478
734
|
usage = inference_results[0].get("usage", {})
|
|
@@ -480,8 +736,8 @@ class OpenSearchAgenticSearcher:
|
|
|
480
736
|
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
481
737
|
completion_tokens = usage.get("completion_tokens", 0)
|
|
482
738
|
|
|
483
|
-
# Parse agentic context for reasoning traces
|
|
484
|
-
agentic_context =
|
|
739
|
+
# Parse agentic context for reasoning traces (if present)
|
|
740
|
+
agentic_context = agent_response.get("agentic_context", {})
|
|
485
741
|
traces = agentic_context.get("traces", [])
|
|
486
742
|
for trace in traces:
|
|
487
743
|
if query.include_reasoning:
|
|
@@ -497,8 +753,8 @@ class OpenSearchAgenticSearcher:
|
|
|
497
753
|
)
|
|
498
754
|
|
|
499
755
|
# If no answer from structured output, try to get from raw response
|
|
500
|
-
if not answer and "response" in
|
|
501
|
-
answer =
|
|
756
|
+
if not answer and "response" in agent_response:
|
|
757
|
+
answer = agent_response.get("response")
|
|
502
758
|
|
|
503
759
|
# Preserve the query's conversation_id if agent didn't return one
|
|
504
760
|
# This allows multi-turn conversations when memory was created beforehand
|
|
@@ -519,6 +775,7 @@ class OpenSearchAgenticSearcher:
|
|
|
519
775
|
total_tokens=total_tokens,
|
|
520
776
|
prompt_tokens=prompt_tokens,
|
|
521
777
|
completion_tokens=completion_tokens,
|
|
778
|
+
generated_query=dsl_string, # Include the generated DSL for debugging
|
|
522
779
|
)
|
|
523
780
|
|
|
524
781
|
def _extract_answer_from_result(
|
|
@@ -622,6 +879,56 @@ class OpenSearchAgenticSearcher:
|
|
|
622
879
|
|
|
623
880
|
return items
|
|
624
881
|
|
|
882
|
+
def _parse_opensearch_hits(
|
|
883
|
+
self,
|
|
884
|
+
hits_data: dict[str, Any],
|
|
885
|
+
) -> list[SearchResultItem]:
|
|
886
|
+
"""Parse OpenSearch hits structure into SearchResultItems.
|
|
887
|
+
|
|
888
|
+
Standard OpenSearch response format:
|
|
889
|
+
{
|
|
890
|
+
"total": {"value": 10},
|
|
891
|
+
"max_score": 1.0,
|
|
892
|
+
"hits": [
|
|
893
|
+
{"_id": "...", "_score": 0.9, "_source": {...}}
|
|
894
|
+
]
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
hits_data: OpenSearch hits object.
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
List of SearchResultItem.
|
|
902
|
+
"""
|
|
903
|
+
items: list[SearchResultItem] = []
|
|
904
|
+
|
|
905
|
+
hits = hits_data.get("hits", [])
|
|
906
|
+
for hit in hits:
|
|
907
|
+
if not isinstance(hit, dict):
|
|
908
|
+
continue
|
|
909
|
+
|
|
910
|
+
source = hit.get("_source", {})
|
|
911
|
+
if not source:
|
|
912
|
+
continue
|
|
913
|
+
|
|
914
|
+
items.append(
|
|
915
|
+
SearchResultItem(
|
|
916
|
+
doc_id=hit.get("_id", ""),
|
|
917
|
+
content=source.get("content", ""),
|
|
918
|
+
score=hit.get("_score", 0.0),
|
|
919
|
+
title=source.get("title"),
|
|
920
|
+
url=source.get("url"),
|
|
921
|
+
source=source.get("source"),
|
|
922
|
+
collection_id=source.get("collection_id"),
|
|
923
|
+
source_id=source.get("source_id"),
|
|
924
|
+
chunk_index=source.get("chunk_index"),
|
|
925
|
+
total_chunks=source.get("total_chunks"),
|
|
926
|
+
metadata=source.get("metadata"),
|
|
927
|
+
)
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
return items
|
|
931
|
+
|
|
625
932
|
|
|
626
933
|
class AgenticSearchFallback:
|
|
627
934
|
"""Fallback handler for when agentic search fails.
|