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.
Files changed (44) hide show
  1. gnosisllm_knowledge/__init__.py +91 -39
  2. gnosisllm_knowledge/api/__init__.py +3 -2
  3. gnosisllm_knowledge/api/knowledge.py +287 -7
  4. gnosisllm_knowledge/api/memory.py +966 -0
  5. gnosisllm_knowledge/backends/__init__.py +14 -5
  6. gnosisllm_knowledge/backends/opensearch/agentic.py +341 -39
  7. gnosisllm_knowledge/backends/opensearch/config.py +49 -28
  8. gnosisllm_knowledge/backends/opensearch/indexer.py +1 -0
  9. gnosisllm_knowledge/backends/opensearch/mappings.py +2 -1
  10. gnosisllm_knowledge/backends/opensearch/memory/__init__.py +12 -0
  11. gnosisllm_knowledge/backends/opensearch/memory/client.py +1380 -0
  12. gnosisllm_knowledge/backends/opensearch/memory/config.py +127 -0
  13. gnosisllm_knowledge/backends/opensearch/memory/setup.py +322 -0
  14. gnosisllm_knowledge/backends/opensearch/searcher.py +235 -0
  15. gnosisllm_knowledge/backends/opensearch/setup.py +308 -148
  16. gnosisllm_knowledge/cli/app.py +378 -12
  17. gnosisllm_knowledge/cli/commands/agentic.py +11 -0
  18. gnosisllm_knowledge/cli/commands/memory.py +723 -0
  19. gnosisllm_knowledge/cli/commands/setup.py +24 -22
  20. gnosisllm_knowledge/cli/display/service.py +43 -0
  21. gnosisllm_knowledge/cli/utils/config.py +58 -0
  22. gnosisllm_knowledge/core/domain/__init__.py +41 -0
  23. gnosisllm_knowledge/core/domain/document.py +5 -0
  24. gnosisllm_knowledge/core/domain/memory.py +440 -0
  25. gnosisllm_knowledge/core/domain/result.py +11 -3
  26. gnosisllm_knowledge/core/domain/search.py +2 -0
  27. gnosisllm_knowledge/core/events/types.py +76 -0
  28. gnosisllm_knowledge/core/exceptions.py +134 -0
  29. gnosisllm_knowledge/core/interfaces/__init__.py +17 -0
  30. gnosisllm_knowledge/core/interfaces/memory.py +524 -0
  31. gnosisllm_knowledge/core/interfaces/streaming.py +127 -0
  32. gnosisllm_knowledge/core/streaming/__init__.py +36 -0
  33. gnosisllm_knowledge/core/streaming/pipeline.py +228 -0
  34. gnosisllm_knowledge/loaders/base.py +3 -4
  35. gnosisllm_knowledge/loaders/sitemap.py +129 -1
  36. gnosisllm_knowledge/loaders/sitemap_streaming.py +258 -0
  37. gnosisllm_knowledge/services/indexing.py +67 -75
  38. gnosisllm_knowledge/services/search.py +47 -11
  39. gnosisllm_knowledge/services/streaming_pipeline.py +302 -0
  40. {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.3.0.dist-info}/METADATA +44 -1
  41. gnosisllm_knowledge-0.3.0.dist-info/RECORD +77 -0
  42. gnosisllm_knowledge-0.2.0.dist-info/RECORD +0 -64
  43. {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.3.0.dist-info}/WHEEL +0 -0
  44. {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. Parse response for answer, reasoning, and results
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
- response = await self._execute_agent(agent_id, execute_body)
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._parse_agentic_response(query, response, duration_ms)
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
- Only includes parameters that the agent actually uses:
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
- Note: VectorDBTool's index and model_id are configured in the agent,
326
- not passed at runtime. Extra parameters cause IllegalArgumentException.
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 - agent has hardcoded index).
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
- response: dict[str, Any],
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
- The response structure from OpenSearch ML agents:
398
- {
399
- "inference_results": [
400
- {
401
- "output": [
402
- {"name": "response", "result": "The answer..."},
403
- {"name": "knowledge_search", "result": {...}}
404
- ]
405
- }
406
- ],
407
- "memory_id": "...",
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
- response: Agent execution response.
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 = response.get("memory_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 inference results
428
- inference_results = response.get("inference_results", [])
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 ("response", "answer_generator", "MLModelTool"):
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 tool output
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, # Not tracked per-step
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 = response.get("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 response:
501
- answer = response.get("response")
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 = "l2" # l2, cosinesimil, innerproduct
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
- Environment variables:
143
- OPENSEARCH_HOST: Host (default: localhost)
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(",")) if nodes_str else None
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
- use_aws_sigv4=os.getenv("OPENSEARCH_USE_AWS_SIGV4", "").lower() == "true",
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
- model_id=os.getenv("OPENSEARCH_MODEL_ID"),
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
- # Agentic search configuration
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,