gnosisllm-knowledge 0.2.0__py3-none-any.whl → 0.3.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 +287 -7
- gnosisllm_knowledge/api/memory.py +966 -0
- gnosisllm_knowledge/backends/__init__.py +14 -5
- gnosisllm_knowledge/backends/opensearch/agentic.py +341 -39
- gnosisllm_knowledge/backends/opensearch/config.py +49 -28
- gnosisllm_knowledge/backends/opensearch/indexer.py +1 -0
- gnosisllm_knowledge/backends/opensearch/mappings.py +2 -1
- 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/searcher.py +235 -0
- gnosisllm_knowledge/backends/opensearch/setup.py +308 -148
- gnosisllm_knowledge/cli/app.py +378 -12
- gnosisllm_knowledge/cli/commands/agentic.py +11 -0
- gnosisllm_knowledge/cli/commands/memory.py +723 -0
- gnosisllm_knowledge/cli/commands/setup.py +24 -22
- gnosisllm_knowledge/cli/display/service.py +43 -0
- gnosisllm_knowledge/cli/utils/config.py +58 -0
- gnosisllm_knowledge/core/domain/__init__.py +41 -0
- gnosisllm_knowledge/core/domain/document.py +5 -0
- gnosisllm_knowledge/core/domain/memory.py +440 -0
- gnosisllm_knowledge/core/domain/result.py +11 -3
- gnosisllm_knowledge/core/domain/search.py +2 -0
- gnosisllm_knowledge/core/events/types.py +76 -0
- gnosisllm_knowledge/core/exceptions.py +134 -0
- gnosisllm_knowledge/core/interfaces/__init__.py +17 -0
- gnosisllm_knowledge/core/interfaces/memory.py +524 -0
- gnosisllm_knowledge/core/interfaces/streaming.py +127 -0
- gnosisllm_knowledge/core/streaming/__init__.py +36 -0
- gnosisllm_knowledge/core/streaming/pipeline.py +228 -0
- gnosisllm_knowledge/loaders/base.py +3 -4
- gnosisllm_knowledge/loaders/sitemap.py +129 -1
- gnosisllm_knowledge/loaders/sitemap_streaming.py +258 -0
- gnosisllm_knowledge/services/indexing.py +67 -75
- gnosisllm_knowledge/services/search.py +47 -11
- gnosisllm_knowledge/services/streaming_pipeline.py +302 -0
- {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.3.0.dist-info}/METADATA +44 -1
- gnosisllm_knowledge-0.3.0.dist-info/RECORD +77 -0
- gnosisllm_knowledge-0.2.0.dist-info/RECORD +0 -64
- {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.3.0.dist-info}/WHEEL +0 -0
- {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -9,18 +9,27 @@ from gnosisllm_knowledge.backends.opensearch import (
|
|
|
9
9
|
OpenSearchKnowledgeSearcher,
|
|
10
10
|
OpenSearchSetupAdapter,
|
|
11
11
|
)
|
|
12
|
+
from gnosisllm_knowledge.backends.opensearch.memory import (
|
|
13
|
+
MemoryConfig,
|
|
14
|
+
MemorySetup,
|
|
15
|
+
OpenSearchMemoryClient,
|
|
16
|
+
)
|
|
12
17
|
from gnosisllm_knowledge.backends.opensearch.queries import QueryBuilder
|
|
13
18
|
|
|
14
19
|
__all__ = [
|
|
20
|
+
"AgenticSearchFallback",
|
|
21
|
+
# OpenSearch Memory
|
|
22
|
+
"MemoryConfig",
|
|
23
|
+
# Memory (for testing)
|
|
24
|
+
"MemoryIndexer",
|
|
25
|
+
"MemorySearcher",
|
|
26
|
+
"MemorySetup",
|
|
27
|
+
"OpenSearchAgenticSearcher",
|
|
15
28
|
# OpenSearch
|
|
16
29
|
"OpenSearchConfig",
|
|
17
30
|
"OpenSearchIndexer",
|
|
18
31
|
"OpenSearchKnowledgeSearcher",
|
|
32
|
+
"OpenSearchMemoryClient",
|
|
19
33
|
"OpenSearchSetupAdapter",
|
|
20
|
-
"OpenSearchAgenticSearcher",
|
|
21
|
-
"AgenticSearchFallback",
|
|
22
34
|
"QueryBuilder",
|
|
23
|
-
# Memory (for testing)
|
|
24
|
-
"MemoryIndexer",
|
|
25
|
-
"MemorySearcher",
|
|
26
35
|
]
|
|
@@ -96,11 +96,12 @@ class OpenSearchAgenticSearcher:
|
|
|
96
96
|
) -> AgenticSearchResult:
|
|
97
97
|
"""Execute agentic search with agent orchestration.
|
|
98
98
|
|
|
99
|
-
The flow:
|
|
99
|
+
The flow with RAGTool:
|
|
100
100
|
1. Select agent based on query.agent_type
|
|
101
101
|
2. Build execution request with query and filters
|
|
102
102
|
3. Execute agent via OpenSearch ML API
|
|
103
|
-
4.
|
|
103
|
+
4. RAGTool searches the index and generates an AI answer
|
|
104
|
+
5. Parse response for answer, reasoning, and source documents
|
|
104
105
|
|
|
105
106
|
Args:
|
|
106
107
|
query: Agentic search query with agent type and context.
|
|
@@ -121,7 +122,7 @@ class OpenSearchAgenticSearcher:
|
|
|
121
122
|
raise AgenticSearchError(
|
|
122
123
|
message=f"Agent not configured for type: {query.agent_type.value}",
|
|
123
124
|
agent_type=query.agent_type.value,
|
|
124
|
-
details={"hint": "Run 'gnosisllm-knowledge agentic setup' to configure agents."},
|
|
125
|
+
details={"hint": "Run 'gnosisllm-knowledge agentic setup --force' to configure agents."},
|
|
125
126
|
)
|
|
126
127
|
|
|
127
128
|
# Build execution request
|
|
@@ -133,15 +134,114 @@ class OpenSearchAgenticSearcher:
|
|
|
133
134
|
"agent_id": agent_id,
|
|
134
135
|
"agent_type": query.agent_type.value,
|
|
135
136
|
"query": query.text[:100],
|
|
137
|
+
"index_name": index_name,
|
|
136
138
|
},
|
|
137
139
|
)
|
|
138
140
|
|
|
139
|
-
# Execute agent
|
|
140
|
-
|
|
141
|
+
# Execute agent - RAGTool handles search AND answer generation
|
|
142
|
+
agent_response = await self._execute_agent(agent_id, execute_body)
|
|
141
143
|
|
|
142
144
|
duration_ms = (datetime.now(UTC) - start).total_seconds() * 1000
|
|
143
145
|
|
|
144
|
-
return self.
|
|
146
|
+
return self._parse_rag_response(query, agent_response, duration_ms)
|
|
147
|
+
|
|
148
|
+
def _parse_rag_response(
|
|
149
|
+
self,
|
|
150
|
+
query: AgenticSearchQuery,
|
|
151
|
+
response: dict[str, Any],
|
|
152
|
+
duration_ms: float,
|
|
153
|
+
) -> AgenticSearchResult:
|
|
154
|
+
"""Parse RAGTool response into AgenticSearchResult.
|
|
155
|
+
|
|
156
|
+
RAGTool returns both an AI-generated answer and source documents.
|
|
157
|
+
|
|
158
|
+
Response format:
|
|
159
|
+
{
|
|
160
|
+
"inference_results": [
|
|
161
|
+
{
|
|
162
|
+
"output": [
|
|
163
|
+
{"name": "knowledge_search", "result": "<LLM answer>"}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
query: The original query.
|
|
171
|
+
response: Agent execution response.
|
|
172
|
+
duration_ms: Total execution duration.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Parsed AgenticSearchResult with answer and sources.
|
|
176
|
+
"""
|
|
177
|
+
answer: str | None = None
|
|
178
|
+
reasoning_steps: list[ReasoningStep] = []
|
|
179
|
+
items: list[SearchResultItem] = []
|
|
180
|
+
conversation_id = response.get("memory_id")
|
|
181
|
+
|
|
182
|
+
# Parse inference results
|
|
183
|
+
inference_results = response.get("inference_results", [])
|
|
184
|
+
if inference_results:
|
|
185
|
+
outputs = inference_results[0].get("output", [])
|
|
186
|
+
|
|
187
|
+
for output in outputs:
|
|
188
|
+
name = output.get("name", "")
|
|
189
|
+
result = output.get("result", "")
|
|
190
|
+
|
|
191
|
+
# Handle dataAsMap structure
|
|
192
|
+
data_as_map = output.get("dataAsMap", {})
|
|
193
|
+
if data_as_map and "response" in data_as_map:
|
|
194
|
+
result = data_as_map.get("response", result)
|
|
195
|
+
|
|
196
|
+
if name == "memory_id":
|
|
197
|
+
conversation_id = str(result) if result else None
|
|
198
|
+
elif name in ("knowledge_search", "RAGTool", "response"):
|
|
199
|
+
# RAGTool returns the LLM-generated answer
|
|
200
|
+
answer = self._extract_answer_from_result(result)
|
|
201
|
+
|
|
202
|
+
if query.include_reasoning:
|
|
203
|
+
reasoning_steps.append(
|
|
204
|
+
ReasoningStep(
|
|
205
|
+
tool="RAGTool",
|
|
206
|
+
action="rag_search",
|
|
207
|
+
input=query.text,
|
|
208
|
+
output=answer[:200] if answer else None,
|
|
209
|
+
duration_ms=duration_ms,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Extract source documents if available in the response
|
|
214
|
+
for output in outputs:
|
|
215
|
+
name = output.get("name", "")
|
|
216
|
+
result = output.get("result", "")
|
|
217
|
+
|
|
218
|
+
# Try to extract source documents from additional_info or similar
|
|
219
|
+
additional_info = output.get("additional_info", {})
|
|
220
|
+
if additional_info:
|
|
221
|
+
hits = additional_info.get("hits", {})
|
|
222
|
+
if hits:
|
|
223
|
+
items.extend(self._parse_opensearch_hits(hits))
|
|
224
|
+
|
|
225
|
+
# If no answer from structured output, try raw response
|
|
226
|
+
if not answer and "response" in response:
|
|
227
|
+
answer = response.get("response")
|
|
228
|
+
|
|
229
|
+
# Preserve the query's conversation_id if agent didn't return one
|
|
230
|
+
final_conversation_id = conversation_id or query.conversation_id
|
|
231
|
+
|
|
232
|
+
return AgenticSearchResult(
|
|
233
|
+
query=query.text,
|
|
234
|
+
mode=SearchMode.AGENTIC,
|
|
235
|
+
items=items,
|
|
236
|
+
total_hits=len(items),
|
|
237
|
+
duration_ms=duration_ms,
|
|
238
|
+
max_score=items[0].score if items else None,
|
|
239
|
+
answer=answer,
|
|
240
|
+
reasoning_steps=reasoning_steps,
|
|
241
|
+
conversation_id=final_conversation_id,
|
|
242
|
+
agent_type=query.agent_type,
|
|
243
|
+
citations=[item.doc_id for item in items[:5]],
|
|
244
|
+
)
|
|
145
245
|
|
|
146
246
|
async def get_conversation(
|
|
147
247
|
self,
|
|
@@ -317,17 +417,18 @@ class OpenSearchAgenticSearcher:
|
|
|
317
417
|
) -> dict[str, Any]:
|
|
318
418
|
"""Build agent execution request.
|
|
319
419
|
|
|
320
|
-
|
|
420
|
+
RAGTool requires:
|
|
321
421
|
- 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
422
|
|
|
325
|
-
|
|
326
|
-
|
|
423
|
+
The index is configured at agent creation time, not at execution time.
|
|
424
|
+
RAGTool searches the configured index and generates an AI answer.
|
|
425
|
+
|
|
426
|
+
Conversational agents also support:
|
|
427
|
+
- memory_id: For conversation continuity
|
|
327
428
|
|
|
328
429
|
Args:
|
|
329
430
|
query: The agentic search query.
|
|
330
|
-
index_name: Target index name (not used
|
|
431
|
+
index_name: Target index name (for logging, not used by RAGTool).
|
|
331
432
|
|
|
332
433
|
Returns:
|
|
333
434
|
Request body for agent execution.
|
|
@@ -339,7 +440,6 @@ class OpenSearchAgenticSearcher:
|
|
|
339
440
|
}
|
|
340
441
|
|
|
341
442
|
# Add conversation context for conversational agents
|
|
342
|
-
# OpenSearch handles memory injection automatically with app_type=rag
|
|
343
443
|
if query.agent_type == AgentType.CONVERSATIONAL and query.conversation_id:
|
|
344
444
|
request["parameters"]["memory_id"] = query.conversation_id
|
|
345
445
|
|
|
@@ -386,32 +486,155 @@ class OpenSearchAgenticSearcher:
|
|
|
386
486
|
cause=e,
|
|
387
487
|
)
|
|
388
488
|
|
|
489
|
+
def _extract_dsl_from_agent_response(
|
|
490
|
+
self,
|
|
491
|
+
response: dict[str, Any],
|
|
492
|
+
) -> dict[str, Any] | None:
|
|
493
|
+
"""Extract generated DSL query from agent response.
|
|
494
|
+
|
|
495
|
+
The flow agent with QueryPlanningTool returns the DSL in the output.
|
|
496
|
+
Format: {"inference_results": [{"output": [{"name": "response", "result": "<DSL JSON>"}]}]}
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
response: Agent execution response.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Parsed DSL query dict, or None if not found.
|
|
503
|
+
"""
|
|
504
|
+
try:
|
|
505
|
+
inference_results = response.get("inference_results", [])
|
|
506
|
+
if not inference_results:
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
outputs = inference_results[0].get("output", [])
|
|
510
|
+
for output in outputs:
|
|
511
|
+
name = output.get("name", "")
|
|
512
|
+
result = output.get("result", "")
|
|
513
|
+
|
|
514
|
+
# QueryPlanningTool outputs come as "response" or "query_planner"
|
|
515
|
+
if name in ("response", "query_planner", "QueryPlanningTool"):
|
|
516
|
+
if isinstance(result, dict):
|
|
517
|
+
return result
|
|
518
|
+
if isinstance(result, str) and result.strip():
|
|
519
|
+
# Try to parse as JSON
|
|
520
|
+
return self._parse_dsl_string(result)
|
|
521
|
+
|
|
522
|
+
return None
|
|
523
|
+
except Exception as e:
|
|
524
|
+
self._logger.warning(f"Failed to extract DSL from agent response: {e}")
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
def _parse_dsl_string(self, dsl_string: str) -> dict[str, Any] | None:
|
|
528
|
+
"""Parse a DSL query string into a dictionary.
|
|
529
|
+
|
|
530
|
+
Handles various formats:
|
|
531
|
+
- Raw JSON
|
|
532
|
+
- Markdown code blocks
|
|
533
|
+
- JSON with surrounding text
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
dsl_string: The DSL query as a string.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Parsed DSL query dict, or None if parsing fails.
|
|
540
|
+
"""
|
|
541
|
+
dsl_string = dsl_string.strip()
|
|
542
|
+
|
|
543
|
+
# Remove markdown code blocks if present
|
|
544
|
+
if dsl_string.startswith("```"):
|
|
545
|
+
lines = dsl_string.split("\n")
|
|
546
|
+
# Remove first line (```json or ```)
|
|
547
|
+
lines = lines[1:] if lines else []
|
|
548
|
+
# Remove last line (```)
|
|
549
|
+
if lines and lines[-1].strip() == "```":
|
|
550
|
+
lines = lines[:-1]
|
|
551
|
+
dsl_string = "\n".join(lines).strip()
|
|
552
|
+
|
|
553
|
+
# Try to find and parse JSON
|
|
554
|
+
try:
|
|
555
|
+
# Find the first { and last }
|
|
556
|
+
start = dsl_string.find("{")
|
|
557
|
+
end = dsl_string.rfind("}") + 1
|
|
558
|
+
if start >= 0 and end > start:
|
|
559
|
+
json_str = dsl_string[start:end]
|
|
560
|
+
return json.loads(json_str)
|
|
561
|
+
except json.JSONDecodeError as e:
|
|
562
|
+
self._logger.debug(f"Failed to parse DSL JSON: {e}")
|
|
563
|
+
|
|
564
|
+
# Try parsing the whole string
|
|
565
|
+
try:
|
|
566
|
+
return json.loads(dsl_string)
|
|
567
|
+
except json.JSONDecodeError:
|
|
568
|
+
pass
|
|
569
|
+
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
async def _execute_dsl_query(
|
|
573
|
+
self,
|
|
574
|
+
index_name: str,
|
|
575
|
+
dsl_query: dict[str, Any],
|
|
576
|
+
) -> dict[str, Any]:
|
|
577
|
+
"""Execute a DSL query against the index.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
index_name: Target index name.
|
|
581
|
+
dsl_query: OpenSearch DSL query to execute.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Search response with hits.
|
|
585
|
+
|
|
586
|
+
Raises:
|
|
587
|
+
AgenticSearchError: If query execution fails.
|
|
588
|
+
"""
|
|
589
|
+
try:
|
|
590
|
+
response = await self._client.search(
|
|
591
|
+
index=index_name,
|
|
592
|
+
body=dsl_query,
|
|
593
|
+
)
|
|
594
|
+
return response
|
|
595
|
+
except Exception as e:
|
|
596
|
+
self._logger.error(f"DSL query execution failed: {e}")
|
|
597
|
+
raise AgenticSearchError(
|
|
598
|
+
message=f"Failed to execute generated DSL query: {e}",
|
|
599
|
+
details={"index_name": index_name, "query": str(dsl_query)[:200]},
|
|
600
|
+
cause=e,
|
|
601
|
+
)
|
|
602
|
+
|
|
389
603
|
def _parse_agentic_response(
|
|
390
604
|
self,
|
|
391
605
|
query: AgenticSearchQuery,
|
|
392
|
-
|
|
606
|
+
agent_response: dict[str, Any],
|
|
393
607
|
duration_ms: float,
|
|
608
|
+
search_response: dict[str, Any] | None = None,
|
|
609
|
+
generated_dsl: dict[str, Any] | None = None,
|
|
394
610
|
) -> AgenticSearchResult:
|
|
395
611
|
"""Parse agent response into AgenticSearchResult.
|
|
396
612
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
613
|
+
Supports two response formats:
|
|
614
|
+
|
|
615
|
+
1. QueryPlanningTool (OpenSearch 3.2+):
|
|
616
|
+
The agent generates DSL queries which we then execute.
|
|
617
|
+
Agent response: {"inference_results": [{"output": [{"name": "response", "result": "<DSL JSON>"}]}]}
|
|
618
|
+
Search response: Standard OpenSearch search response with hits.
|
|
619
|
+
|
|
620
|
+
2. Legacy VectorDBTool + MLModelTool:
|
|
621
|
+
{
|
|
622
|
+
"inference_results": [
|
|
623
|
+
{
|
|
624
|
+
"output": [
|
|
625
|
+
{"name": "knowledge_search", "result": {...}},
|
|
626
|
+
{"name": "answer_generator", "result": "..."}
|
|
627
|
+
]
|
|
628
|
+
}
|
|
629
|
+
]
|
|
630
|
+
}
|
|
410
631
|
|
|
411
632
|
Args:
|
|
412
633
|
query: The original query.
|
|
413
|
-
|
|
634
|
+
agent_response: Agent execution response.
|
|
414
635
|
duration_ms: Total execution duration.
|
|
636
|
+
search_response: Search results from executing the generated DSL (optional).
|
|
637
|
+
generated_dsl: The DSL query generated by the agent (optional).
|
|
415
638
|
|
|
416
639
|
Returns:
|
|
417
640
|
Parsed AgenticSearchResult.
|
|
@@ -419,13 +642,40 @@ class OpenSearchAgenticSearcher:
|
|
|
419
642
|
answer: str | None = None
|
|
420
643
|
reasoning_steps: list[ReasoningStep] = []
|
|
421
644
|
items: list[SearchResultItem] = []
|
|
422
|
-
conversation_id =
|
|
645
|
+
conversation_id = agent_response.get("memory_id")
|
|
646
|
+
dsl_string: str | None = None
|
|
423
647
|
total_tokens = 0
|
|
424
648
|
prompt_tokens = 0
|
|
425
649
|
completion_tokens = 0
|
|
426
650
|
|
|
427
|
-
# Parse
|
|
428
|
-
|
|
651
|
+
# Parse search results from executed DSL query first (QueryPlanningTool flow)
|
|
652
|
+
if search_response:
|
|
653
|
+
hits_data = search_response.get("hits", {})
|
|
654
|
+
items.extend(self._parse_opensearch_hits(hits_data))
|
|
655
|
+
|
|
656
|
+
if query.include_reasoning:
|
|
657
|
+
dsl_string = json.dumps(generated_dsl) if generated_dsl else None
|
|
658
|
+
reasoning_steps.append(
|
|
659
|
+
ReasoningStep(
|
|
660
|
+
tool="QueryPlanningTool",
|
|
661
|
+
action="query_generation",
|
|
662
|
+
input=query.text,
|
|
663
|
+
output=dsl_string[:200] if dsl_string else None,
|
|
664
|
+
duration_ms=0,
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
reasoning_steps.append(
|
|
668
|
+
ReasoningStep(
|
|
669
|
+
tool="QueryPlanningTool",
|
|
670
|
+
action="search_execution",
|
|
671
|
+
input=dsl_string[:100] if dsl_string else query.text,
|
|
672
|
+
output=f"Found {len(items)} documents",
|
|
673
|
+
duration_ms=0,
|
|
674
|
+
)
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
# Parse inference results for additional outputs (legacy or conversational)
|
|
678
|
+
inference_results = agent_response.get("inference_results", [])
|
|
429
679
|
if inference_results:
|
|
430
680
|
outputs = inference_results[0].get("output", [])
|
|
431
681
|
|
|
@@ -443,8 +693,8 @@ class OpenSearchAgenticSearcher:
|
|
|
443
693
|
elif name == "parent_message_id":
|
|
444
694
|
# Track parent message ID for conversation threading
|
|
445
695
|
pass # Could store for future use
|
|
446
|
-
elif name in ("
|
|
447
|
-
# Parse answer from output
|
|
696
|
+
elif name in ("answer_generator", "MLModelTool"):
|
|
697
|
+
# Parse answer from output (legacy format)
|
|
448
698
|
answer = self._extract_answer_from_result(result)
|
|
449
699
|
|
|
450
700
|
# Add reasoning step for answer generation
|
|
@@ -459,7 +709,7 @@ class OpenSearchAgenticSearcher:
|
|
|
459
709
|
)
|
|
460
710
|
)
|
|
461
711
|
elif name in ("knowledge_search", "VectorDBTool"):
|
|
462
|
-
# Parse search results from
|
|
712
|
+
# Parse search results from legacy VectorDBTool output
|
|
463
713
|
items.extend(self._parse_tool_search_results(result))
|
|
464
714
|
|
|
465
715
|
# Add reasoning step
|
|
@@ -470,9 +720,10 @@ class OpenSearchAgenticSearcher:
|
|
|
470
720
|
action="search",
|
|
471
721
|
input=query.text,
|
|
472
722
|
output=f"Found {len(items)} documents",
|
|
473
|
-
duration_ms=0,
|
|
723
|
+
duration_ms=0,
|
|
474
724
|
)
|
|
475
725
|
)
|
|
726
|
+
# Skip "response" and "query_planner" here - they're handled via generated_dsl parameter
|
|
476
727
|
|
|
477
728
|
# Parse token usage if available
|
|
478
729
|
usage = inference_results[0].get("usage", {})
|
|
@@ -480,8 +731,8 @@ class OpenSearchAgenticSearcher:
|
|
|
480
731
|
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
481
732
|
completion_tokens = usage.get("completion_tokens", 0)
|
|
482
733
|
|
|
483
|
-
# Parse agentic context for reasoning traces
|
|
484
|
-
agentic_context =
|
|
734
|
+
# Parse agentic context for reasoning traces (if present)
|
|
735
|
+
agentic_context = agent_response.get("agentic_context", {})
|
|
485
736
|
traces = agentic_context.get("traces", [])
|
|
486
737
|
for trace in traces:
|
|
487
738
|
if query.include_reasoning:
|
|
@@ -497,8 +748,8 @@ class OpenSearchAgenticSearcher:
|
|
|
497
748
|
)
|
|
498
749
|
|
|
499
750
|
# If no answer from structured output, try to get from raw response
|
|
500
|
-
if not answer and "response" in
|
|
501
|
-
answer =
|
|
751
|
+
if not answer and "response" in agent_response:
|
|
752
|
+
answer = agent_response.get("response")
|
|
502
753
|
|
|
503
754
|
# Preserve the query's conversation_id if agent didn't return one
|
|
504
755
|
# This allows multi-turn conversations when memory was created beforehand
|
|
@@ -519,6 +770,7 @@ class OpenSearchAgenticSearcher:
|
|
|
519
770
|
total_tokens=total_tokens,
|
|
520
771
|
prompt_tokens=prompt_tokens,
|
|
521
772
|
completion_tokens=completion_tokens,
|
|
773
|
+
generated_query=dsl_string, # Include the generated DSL for debugging
|
|
522
774
|
)
|
|
523
775
|
|
|
524
776
|
def _extract_answer_from_result(
|
|
@@ -622,6 +874,56 @@ class OpenSearchAgenticSearcher:
|
|
|
622
874
|
|
|
623
875
|
return items
|
|
624
876
|
|
|
877
|
+
def _parse_opensearch_hits(
|
|
878
|
+
self,
|
|
879
|
+
hits_data: dict[str, Any],
|
|
880
|
+
) -> list[SearchResultItem]:
|
|
881
|
+
"""Parse OpenSearch hits structure into SearchResultItems.
|
|
882
|
+
|
|
883
|
+
Standard OpenSearch response format:
|
|
884
|
+
{
|
|
885
|
+
"total": {"value": 10},
|
|
886
|
+
"max_score": 1.0,
|
|
887
|
+
"hits": [
|
|
888
|
+
{"_id": "...", "_score": 0.9, "_source": {...}}
|
|
889
|
+
]
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
hits_data: OpenSearch hits object.
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
List of SearchResultItem.
|
|
897
|
+
"""
|
|
898
|
+
items: list[SearchResultItem] = []
|
|
899
|
+
|
|
900
|
+
hits = hits_data.get("hits", [])
|
|
901
|
+
for hit in hits:
|
|
902
|
+
if not isinstance(hit, dict):
|
|
903
|
+
continue
|
|
904
|
+
|
|
905
|
+
source = hit.get("_source", {})
|
|
906
|
+
if not source:
|
|
907
|
+
continue
|
|
908
|
+
|
|
909
|
+
items.append(
|
|
910
|
+
SearchResultItem(
|
|
911
|
+
doc_id=hit.get("_id", ""),
|
|
912
|
+
content=source.get("content", ""),
|
|
913
|
+
score=hit.get("_score", 0.0),
|
|
914
|
+
title=source.get("title"),
|
|
915
|
+
url=source.get("url"),
|
|
916
|
+
source=source.get("source"),
|
|
917
|
+
collection_id=source.get("collection_id"),
|
|
918
|
+
source_id=source.get("source_id"),
|
|
919
|
+
chunk_index=source.get("chunk_index"),
|
|
920
|
+
total_chunks=source.get("total_chunks"),
|
|
921
|
+
metadata=source.get("metadata"),
|
|
922
|
+
)
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
return items
|
|
926
|
+
|
|
625
927
|
|
|
626
928
|
class AgenticSearchFallback:
|
|
627
929
|
"""Fallback handler for when agentic search fails.
|
|
@@ -74,7 +74,7 @@ class OpenSearchConfig:
|
|
|
74
74
|
|
|
75
75
|
# === k-NN Settings ===
|
|
76
76
|
knn_engine: str = "lucene" # lucene (recommended for OpenSearch 2.9+), faiss
|
|
77
|
-
knn_space_type: str = "
|
|
77
|
+
knn_space_type: str = "cosinesimil" # cosinesimil (recommended), l2, innerproduct
|
|
78
78
|
knn_algo_param_ef_search: int = 512
|
|
79
79
|
knn_algo_param_ef_construction: int = 512
|
|
80
80
|
knn_algo_param_m: int = 16
|
|
@@ -85,10 +85,13 @@ class OpenSearchConfig:
|
|
|
85
85
|
model_group_id: str | None = None
|
|
86
86
|
embedding_field: str = "content_embedding" # Field name for embeddings
|
|
87
87
|
|
|
88
|
-
# === Agentic Search ===
|
|
88
|
+
# === Agentic Search (OpenSearch 3.2+) ===
|
|
89
|
+
# Uses QueryPlanningTool for LLM-generated DSL queries
|
|
89
90
|
# Agent IDs from 'gnosisllm-knowledge agentic setup'
|
|
90
91
|
flow_agent_id: str | None = None
|
|
91
92
|
conversational_agent_id: str | None = None
|
|
93
|
+
# Agentic search pipeline (created during agentic setup)
|
|
94
|
+
agentic_pipeline_name: str | None = None
|
|
92
95
|
# LLM for agent reasoning (OpenAI model ID)
|
|
93
96
|
agentic_llm_model: str = "gpt-4o"
|
|
94
97
|
# Agent execution limits
|
|
@@ -139,57 +142,75 @@ class OpenSearchConfig:
|
|
|
139
142
|
def from_env(cls) -> OpenSearchConfig:
|
|
140
143
|
"""Create config from environment variables.
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
OPENSEARCH_PORT: Port (default: 9200)
|
|
145
|
-
OPENSEARCH_USE_SSL: Use SSL (default: false)
|
|
146
|
-
OPENSEARCH_VERIFY_CERTS: Verify certificates (default: true)
|
|
147
|
-
OPENSEARCH_USERNAME: Username
|
|
148
|
-
OPENSEARCH_PASSWORD: Password
|
|
149
|
-
OPENSEARCH_USE_AWS_SIGV4: Use AWS Sig v4 auth (default: false)
|
|
150
|
-
AWS_REGION: AWS region for Sig v4
|
|
151
|
-
OPENSEARCH_NODES: Comma-separated list of nodes
|
|
152
|
-
EMBEDDING_MODEL: OpenAI embedding model
|
|
153
|
-
EMBEDDING_DIMENSION: Embedding vector dimension
|
|
154
|
-
OPENAI_API_KEY: OpenAI API key
|
|
155
|
-
OPENSEARCH_INDEX_PREFIX: Index name prefix
|
|
156
|
-
OPENSEARCH_SHARDS: Number of shards
|
|
157
|
-
OPENSEARCH_REPLICAS: Number of replicas
|
|
158
|
-
OPENSEARCH_FLOW_AGENT_ID: Flow agent ID for agentic search
|
|
159
|
-
OPENSEARCH_CONVERSATIONAL_AGENT_ID: Conversational agent ID
|
|
160
|
-
AGENTIC_LLM_MODEL: LLM model for agent reasoning (default: gpt-4o)
|
|
161
|
-
AGENTIC_MAX_ITERATIONS: Maximum agent iterations (default: 5)
|
|
162
|
-
AGENTIC_TIMEOUT_SECONDS: Agent execution timeout (default: 60)
|
|
145
|
+
All configuration options can be set via environment variables.
|
|
146
|
+
See .env.example for a complete list with descriptions.
|
|
163
147
|
|
|
164
148
|
Returns:
|
|
165
149
|
Configuration from environment.
|
|
166
150
|
"""
|
|
151
|
+
# Parse nodes list
|
|
167
152
|
nodes_str = os.getenv("OPENSEARCH_NODES", "")
|
|
168
|
-
nodes = tuple(nodes_str.split(",")
|
|
153
|
+
nodes = tuple(n.strip() for n in nodes_str.split(",") if n.strip()) or None
|
|
169
154
|
|
|
170
155
|
return cls(
|
|
156
|
+
# === Connection ===
|
|
171
157
|
host=os.getenv("OPENSEARCH_HOST", "localhost"),
|
|
172
158
|
port=int(os.getenv("OPENSEARCH_PORT", "9200")),
|
|
173
|
-
use_ssl=os.getenv("OPENSEARCH_USE_SSL", "").lower() == "true",
|
|
159
|
+
use_ssl=os.getenv("OPENSEARCH_USE_SSL", "false").lower() == "true",
|
|
174
160
|
verify_certs=os.getenv("OPENSEARCH_VERIFY_CERTS", "true").lower() == "true",
|
|
161
|
+
ca_certs=os.getenv("OPENSEARCH_CA_CERTS"),
|
|
162
|
+
# Authentication
|
|
175
163
|
username=os.getenv("OPENSEARCH_USERNAME"),
|
|
176
164
|
password=os.getenv("OPENSEARCH_PASSWORD"),
|
|
177
|
-
|
|
165
|
+
# AWS OpenSearch Service
|
|
166
|
+
use_aws_sigv4=os.getenv("OPENSEARCH_USE_AWS_SIGV4", "false").lower() == "true",
|
|
178
167
|
aws_region=os.getenv("AWS_REGION"),
|
|
168
|
+
aws_service=os.getenv("OPENSEARCH_AWS_SERVICE", "es"),
|
|
169
|
+
# === Cluster (High Availability) ===
|
|
179
170
|
nodes=nodes,
|
|
171
|
+
sniff_on_start=os.getenv("OPENSEARCH_SNIFF_ON_START", "false").lower() == "true",
|
|
172
|
+
sniff_on_node_failure=os.getenv("OPENSEARCH_SNIFF_ON_NODE_FAILURE", "true").lower()
|
|
173
|
+
== "true",
|
|
174
|
+
sniff_timeout=float(os.getenv("OPENSEARCH_SNIFF_TIMEOUT", "10.0")),
|
|
175
|
+
sniffer_timeout=float(os.getenv("OPENSEARCH_SNIFFER_TIMEOUT", "60.0")),
|
|
176
|
+
# === Embedding ===
|
|
180
177
|
embedding_model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-small"),
|
|
181
178
|
embedding_dimension=int(os.getenv("EMBEDDING_DIMENSION", "1536")),
|
|
182
179
|
openai_api_key=os.getenv("OPENAI_API_KEY"),
|
|
180
|
+
embedding_batch_size=int(os.getenv("EMBEDDING_BATCH_SIZE", "100")),
|
|
181
|
+
# === Index Settings ===
|
|
183
182
|
index_prefix=os.getenv("OPENSEARCH_INDEX_PREFIX", "gnosisllm"),
|
|
184
183
|
number_of_shards=int(os.getenv("OPENSEARCH_SHARDS", "5")),
|
|
185
184
|
number_of_replicas=int(os.getenv("OPENSEARCH_REPLICAS", "1")),
|
|
186
|
-
|
|
185
|
+
refresh_interval=os.getenv("OPENSEARCH_REFRESH_INTERVAL", "1s"),
|
|
186
|
+
# Pipeline names
|
|
187
187
|
ingest_pipeline_name=os.getenv("OPENSEARCH_INGEST_PIPELINE"),
|
|
188
188
|
search_pipeline_name=os.getenv("OPENSEARCH_SEARCH_PIPELINE"),
|
|
189
|
-
#
|
|
189
|
+
# === k-NN Settings ===
|
|
190
|
+
knn_engine=os.getenv("OPENSEARCH_KNN_ENGINE", "lucene"),
|
|
191
|
+
knn_space_type=os.getenv("OPENSEARCH_KNN_SPACE_TYPE", "cosinesimil"),
|
|
192
|
+
knn_algo_param_ef_search=int(os.getenv("OPENSEARCH_KNN_EF_SEARCH", "512")),
|
|
193
|
+
knn_algo_param_ef_construction=int(
|
|
194
|
+
os.getenv("OPENSEARCH_KNN_EF_CONSTRUCTION", "512")
|
|
195
|
+
),
|
|
196
|
+
knn_algo_param_m=int(os.getenv("OPENSEARCH_KNN_M", "16")),
|
|
197
|
+
# === Neural Search ===
|
|
198
|
+
model_id=os.getenv("OPENSEARCH_MODEL_ID"),
|
|
199
|
+
model_group_id=os.getenv("OPENSEARCH_MODEL_GROUP_ID"),
|
|
200
|
+
embedding_field=os.getenv("OPENSEARCH_EMBEDDING_FIELD", "content_embedding"),
|
|
201
|
+
# === Agentic Search ===
|
|
190
202
|
flow_agent_id=os.getenv("OPENSEARCH_FLOW_AGENT_ID"),
|
|
191
203
|
conversational_agent_id=os.getenv("OPENSEARCH_CONVERSATIONAL_AGENT_ID"),
|
|
204
|
+
agentic_pipeline_name=os.getenv("OPENSEARCH_AGENTIC_PIPELINE"),
|
|
192
205
|
agentic_llm_model=os.getenv("AGENTIC_LLM_MODEL", "gpt-4o"),
|
|
193
206
|
agentic_max_iterations=int(os.getenv("AGENTIC_MAX_ITERATIONS", "5")),
|
|
194
207
|
agentic_timeout_seconds=int(os.getenv("AGENTIC_TIMEOUT_SECONDS", "60")),
|
|
208
|
+
memory_window_size=int(os.getenv("AGENTIC_MEMORY_WINDOW_SIZE", "10")),
|
|
209
|
+
# === Timeouts ===
|
|
210
|
+
connect_timeout=float(os.getenv("OPENSEARCH_CONNECT_TIMEOUT", "5.0")),
|
|
211
|
+
read_timeout=float(os.getenv("OPENSEARCH_READ_TIMEOUT", "30.0")),
|
|
212
|
+
bulk_timeout=float(os.getenv("OPENSEARCH_BULK_TIMEOUT", "120.0")),
|
|
213
|
+
# === Bulk Indexing ===
|
|
214
|
+
bulk_batch_size=int(os.getenv("OPENSEARCH_BULK_BATCH_SIZE", "500")),
|
|
215
|
+
bulk_max_concurrent=int(os.getenv("OPENSEARCH_BULK_MAX_CONCURRENT", "3")),
|
|
195
216
|
)
|
|
@@ -481,6 +481,7 @@ class OpenSearchIndexer:
|
|
|
481
481
|
"source": document.source,
|
|
482
482
|
"account_id": document.account_id,
|
|
483
483
|
"collection_id": document.collection_id,
|
|
484
|
+
"collection_name": document.collection_name,
|
|
484
485
|
"source_id": document.source_id,
|
|
485
486
|
"chunk_index": document.chunk_index,
|
|
486
487
|
"total_chunks": document.total_chunks,
|