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.
Files changed (63) hide show
  1. gnosisllm_knowledge/__init__.py +91 -39
  2. gnosisllm_knowledge/api/__init__.py +3 -2
  3. gnosisllm_knowledge/api/knowledge.py +502 -32
  4. gnosisllm_knowledge/api/memory.py +966 -0
  5. gnosisllm_knowledge/backends/__init__.py +14 -5
  6. gnosisllm_knowledge/backends/memory/indexer.py +27 -2
  7. gnosisllm_knowledge/backends/memory/searcher.py +111 -10
  8. gnosisllm_knowledge/backends/opensearch/agentic.py +355 -48
  9. gnosisllm_knowledge/backends/opensearch/config.py +49 -28
  10. gnosisllm_knowledge/backends/opensearch/indexer.py +49 -3
  11. gnosisllm_knowledge/backends/opensearch/mappings.py +14 -5
  12. gnosisllm_knowledge/backends/opensearch/memory/__init__.py +12 -0
  13. gnosisllm_knowledge/backends/opensearch/memory/client.py +1380 -0
  14. gnosisllm_knowledge/backends/opensearch/memory/config.py +127 -0
  15. gnosisllm_knowledge/backends/opensearch/memory/setup.py +322 -0
  16. gnosisllm_knowledge/backends/opensearch/queries.py +33 -33
  17. gnosisllm_knowledge/backends/opensearch/searcher.py +238 -0
  18. gnosisllm_knowledge/backends/opensearch/setup.py +308 -148
  19. gnosisllm_knowledge/cli/app.py +436 -31
  20. gnosisllm_knowledge/cli/commands/agentic.py +26 -9
  21. gnosisllm_knowledge/cli/commands/load.py +169 -19
  22. gnosisllm_knowledge/cli/commands/memory.py +733 -0
  23. gnosisllm_knowledge/cli/commands/search.py +9 -10
  24. gnosisllm_knowledge/cli/commands/setup.py +49 -23
  25. gnosisllm_knowledge/cli/display/service.py +43 -0
  26. gnosisllm_knowledge/cli/utils/config.py +62 -4
  27. gnosisllm_knowledge/core/domain/__init__.py +54 -0
  28. gnosisllm_knowledge/core/domain/discovery.py +166 -0
  29. gnosisllm_knowledge/core/domain/document.py +19 -19
  30. gnosisllm_knowledge/core/domain/memory.py +440 -0
  31. gnosisllm_knowledge/core/domain/result.py +11 -3
  32. gnosisllm_knowledge/core/domain/search.py +12 -25
  33. gnosisllm_knowledge/core/domain/source.py +11 -12
  34. gnosisllm_knowledge/core/events/__init__.py +8 -0
  35. gnosisllm_knowledge/core/events/types.py +198 -5
  36. gnosisllm_knowledge/core/exceptions.py +227 -0
  37. gnosisllm_knowledge/core/interfaces/__init__.py +17 -0
  38. gnosisllm_knowledge/core/interfaces/agentic.py +11 -3
  39. gnosisllm_knowledge/core/interfaces/indexer.py +10 -1
  40. gnosisllm_knowledge/core/interfaces/memory.py +524 -0
  41. gnosisllm_knowledge/core/interfaces/searcher.py +10 -1
  42. gnosisllm_knowledge/core/interfaces/streaming.py +133 -0
  43. gnosisllm_knowledge/core/streaming/__init__.py +36 -0
  44. gnosisllm_knowledge/core/streaming/pipeline.py +228 -0
  45. gnosisllm_knowledge/fetchers/__init__.py +8 -0
  46. gnosisllm_knowledge/fetchers/config.py +27 -0
  47. gnosisllm_knowledge/fetchers/neoreader.py +31 -3
  48. gnosisllm_knowledge/fetchers/neoreader_discovery.py +505 -0
  49. gnosisllm_knowledge/loaders/__init__.py +5 -1
  50. gnosisllm_knowledge/loaders/base.py +3 -4
  51. gnosisllm_knowledge/loaders/discovery.py +338 -0
  52. gnosisllm_knowledge/loaders/discovery_streaming.py +343 -0
  53. gnosisllm_knowledge/loaders/factory.py +46 -0
  54. gnosisllm_knowledge/loaders/sitemap.py +129 -1
  55. gnosisllm_knowledge/loaders/sitemap_streaming.py +258 -0
  56. gnosisllm_knowledge/services/indexing.py +100 -93
  57. gnosisllm_knowledge/services/search.py +84 -31
  58. gnosisllm_knowledge/services/streaming_pipeline.py +334 -0
  59. {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.4.0.dist-info}/METADATA +73 -10
  60. gnosisllm_knowledge-0.4.0.dist-info/RECORD +81 -0
  61. gnosisllm_knowledge-0.2.0.dist-info/RECORD +0 -64
  62. {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.4.0.dist-info}/WHEEL +0 -0
  63. {gnosisllm_knowledge-0.2.0.dist-info → gnosisllm_knowledge-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,11 @@
1
1
  """GnosisLLM Knowledge CLI Application.
2
2
 
3
3
  Main entry point assembling all CLI commands with enterprise-grade UX.
4
+
5
+ Note:
6
+ This library is tenant-agnostic. Multi-tenancy is achieved through index
7
+ isolation - each tenant should use a separate index (e.g., "knowledge-{account_id}").
8
+ Use --index to target tenant-specific indices.
4
9
  """
5
10
 
6
11
  from __future__ import annotations
@@ -63,13 +68,13 @@ def main_callback(
63
68
  @app.command()
64
69
  def setup(
65
70
  host: Annotated[
66
- str,
67
- typer.Option("--host", "-h", help="OpenSearch host."),
68
- ] = "localhost",
71
+ Optional[str],
72
+ typer.Option("--host", "-h", help="OpenSearch host (default: from OPENSEARCH_HOST env)."),
73
+ ] = None,
69
74
  port: Annotated[
70
- int,
71
- typer.Option("--port", "-p", help="OpenSearch port."),
72
- ] = 9200,
75
+ Optional[int],
76
+ typer.Option("--port", "-p", help="OpenSearch port (default: from OPENSEARCH_PORT env)."),
77
+ ] = None,
73
78
  username: Annotated[
74
79
  Optional[str],
75
80
  typer.Option("--username", "-u", help="OpenSearch username."),
@@ -79,13 +84,13 @@ def setup(
79
84
  typer.Option("--password", help="OpenSearch password."),
80
85
  ] = None,
81
86
  use_ssl: Annotated[
82
- bool,
83
- typer.Option("--use-ssl", help="Enable SSL."),
84
- ] = False,
87
+ Optional[bool],
88
+ typer.Option("--use-ssl/--no-ssl", help="Enable/disable SSL (default: from OPENSEARCH_USE_SSL env)."),
89
+ ] = None,
85
90
  verify_certs: Annotated[
86
- bool,
87
- typer.Option("--verify-certs", help="Verify SSL certificates."),
88
- ] = False,
91
+ Optional[bool],
92
+ typer.Option("--verify-certs/--no-verify-certs", help="Verify SSL certificates."),
93
+ ] = None,
89
94
  force: Annotated[
90
95
  bool,
91
96
  typer.Option("--force", "-f", help="Clean up existing resources first."),
@@ -147,17 +152,13 @@ def load(
147
152
  typer.Option(
148
153
  "--type",
149
154
  "-t",
150
- help="Source type: website, sitemap (auto-detects if not specified).",
155
+ help="Source type: website, sitemap, discovery (auto-detects if not specified).",
151
156
  ),
152
157
  ] = None,
153
158
  index: Annotated[
154
159
  str,
155
- typer.Option("--index", "-i", help="Target index name."),
160
+ typer.Option("--index", "-i", help="Target index name (use tenant-specific name for multi-tenancy)."),
156
161
  ] = "knowledge",
157
- account_id: Annotated[
158
- Optional[str],
159
- typer.Option("--account-id", "-a", help="Multi-tenant account ID."),
160
- ] = None,
161
162
  collection_id: Annotated[
162
163
  Optional[str],
163
164
  typer.Option("--collection-id", "-c", help="Collection grouping ID."),
@@ -186,16 +187,50 @@ def load(
186
187
  bool,
187
188
  typer.Option("--verbose", "-V", help="Show per-document progress."),
188
189
  ] = False,
190
+ discovery: Annotated[
191
+ bool,
192
+ typer.Option(
193
+ "--discovery",
194
+ "-D",
195
+ help="Use discovery loader to crawl and discover all URLs from the website.",
196
+ ),
197
+ ] = False,
198
+ max_depth: Annotated[
199
+ int,
200
+ typer.Option("--max-depth", help="Maximum crawl depth for discovery (default: 3)."),
201
+ ] = 3,
202
+ max_pages: Annotated[
203
+ int,
204
+ typer.Option("--max-pages", help="Maximum pages to discover (default: 100)."),
205
+ ] = 100,
206
+ same_domain: Annotated[
207
+ bool,
208
+ typer.Option(
209
+ "--same-domain/--any-domain",
210
+ help="Only crawl URLs on the same domain (default: same domain only).",
211
+ ),
212
+ ] = True,
189
213
  ) -> None:
190
214
  """Load and index content from URLs or sitemaps.
191
215
 
192
216
  Fetches content, chunks it for optimal embedding, and indexes
193
217
  into OpenSearch with automatic embedding generation.
194
218
 
219
+ [bold]Multi-tenancy:[/bold]
220
+ Use --index with tenant-specific index names for isolation
221
+ (e.g., --index knowledge-{account_id}). Each tenant's data
222
+ is stored in a separate index for complete isolation.
223
+
224
+ [bold]Discovery Mode:[/bold]
225
+ Use --discovery to crawl and discover all URLs from a website
226
+ before loading. This is useful for sites without a sitemap.
227
+
195
228
  [bold]Example:[/bold]
196
229
  $ gnosisllm-knowledge load https://docs.example.com/intro
197
230
  $ gnosisllm-knowledge load https://example.com/sitemap.xml --type sitemap
198
231
  $ gnosisllm-knowledge load https://docs.example.com/sitemap.xml --max-urls 500
232
+ $ gnosisllm-knowledge load https://docs.example.com --discovery --max-depth 5
233
+ $ gnosisllm-knowledge load https://docs.example.com --index knowledge-tenant-123
199
234
  """
200
235
  from gnosisllm_knowledge.cli.commands.load import load_command
201
236
 
@@ -205,7 +240,6 @@ def load(
205
240
  source=source,
206
241
  source_type=source_type,
207
242
  index_name=index,
208
- account_id=account_id,
209
243
  collection_id=collection_id,
210
244
  source_id=source_id,
211
245
  batch_size=batch_size,
@@ -213,6 +247,10 @@ def load(
213
247
  force=force,
214
248
  dry_run=dry_run,
215
249
  verbose=verbose,
250
+ discovery=discovery,
251
+ max_depth=max_depth,
252
+ max_pages=max_pages,
253
+ same_domain=same_domain,
216
254
  )
217
255
  )
218
256
 
@@ -238,7 +276,7 @@ def search(
238
276
  ] = "hybrid",
239
277
  index: Annotated[
240
278
  str,
241
- typer.Option("--index", "-i", help="Index to search."),
279
+ typer.Option("--index", "-i", help="Index to search (use tenant-specific name for multi-tenancy)."),
242
280
  ] = "knowledge",
243
281
  limit: Annotated[
244
282
  int,
@@ -248,10 +286,6 @@ def search(
248
286
  int,
249
287
  typer.Option("--offset", "-o", help="Pagination offset."),
250
288
  ] = 0,
251
- account_id: Annotated[
252
- Optional[str],
253
- typer.Option("--account-id", "-a", help="Filter by account ID."),
254
- ] = None,
255
289
  collection_ids: Annotated[
256
290
  Optional[str],
257
291
  typer.Option("--collection-ids", "-c", help="Filter by collection IDs (comma-separated)."),
@@ -289,10 +323,16 @@ def search(
289
323
  - [cyan]hybrid[/cyan]: Combined semantic + keyword (default, best results)
290
324
  - [cyan]agentic[/cyan]: AI-powered search with reasoning
291
325
 
326
+ [bold]Multi-tenancy:[/bold]
327
+ Use --index with tenant-specific index names for isolation
328
+ (e.g., --index knowledge-{account_id}). Each tenant's data
329
+ is stored in a separate index for complete isolation.
330
+
292
331
  [bold]Example:[/bold]
293
332
  $ gnosisllm-knowledge search "how to configure auth"
294
333
  $ gnosisllm-knowledge search "API reference" --mode semantic --limit 10
295
334
  $ gnosisllm-knowledge search --interactive
335
+ $ gnosisllm-knowledge search "query" --index knowledge-tenant-123
296
336
  """
297
337
  from gnosisllm_knowledge.cli.commands.search import search_command
298
338
 
@@ -304,7 +344,6 @@ def search(
304
344
  index_name=index,
305
345
  limit=limit,
306
346
  offset=offset,
307
- account_id=account_id,
308
347
  collection_ids=collection_ids,
309
348
  source_ids=source_ids,
310
349
  min_score=min_score,
@@ -367,6 +406,18 @@ def info() -> None:
367
406
 
368
407
  display.newline()
369
408
 
409
+ display.table(
410
+ "Agentic Memory Configuration",
411
+ [
412
+ ("LLM Model ID", config.memory_llm_model_id or "[dim]Not set[/dim]"),
413
+ ("Embedding Model ID", config.memory_embedding_model_id or "[dim]Not set[/dim]"),
414
+ ("LLM Model", config.memory_llm_model),
415
+ ("Embedding Model", config.memory_embedding_model),
416
+ ],
417
+ )
418
+
419
+ display.newline()
420
+
370
421
  display.table(
371
422
  "Content Fetching",
372
423
  [
@@ -439,7 +490,7 @@ def agentic_setup(
439
490
  def agentic_chat(
440
491
  index: Annotated[
441
492
  str,
442
- typer.Option("--index", "-i", help="Index to search."),
493
+ typer.Option("--index", "-i", help="Index to search (use tenant-specific name for multi-tenancy)."),
443
494
  ] = "knowledge",
444
495
  agent_type: Annotated[
445
496
  str,
@@ -449,10 +500,6 @@ def agentic_chat(
449
500
  help="Agent type: flow or conversational (default).",
450
501
  ),
451
502
  ] = "conversational",
452
- account_id: Annotated[
453
- Optional[str],
454
- typer.Option("--account-id", "-a", help="Filter by account ID."),
455
- ] = None,
456
503
  collection_ids: Annotated[
457
504
  Optional[str],
458
505
  typer.Option("--collection-ids", "-c", help="Filter by collection IDs (comma-separated)."),
@@ -467,10 +514,15 @@ def agentic_chat(
467
514
  Start a conversation with the AI-powered knowledge assistant.
468
515
  The agent remembers context for multi-turn dialogue.
469
516
 
517
+ [bold]Multi-tenancy:[/bold]
518
+ Use --index with tenant-specific index names for isolation
519
+ (e.g., --index knowledge-{account_id}).
520
+
470
521
  [bold]Example:[/bold]
471
522
  $ gnosisllm-knowledge agentic chat
472
523
  $ gnosisllm-knowledge agentic chat --type flow
473
524
  $ gnosisllm-knowledge agentic chat --verbose
525
+ $ gnosisllm-knowledge agentic chat --index knowledge-tenant-123
474
526
  """
475
527
  from gnosisllm_knowledge.cli.commands.agentic import agentic_chat_command
476
528
 
@@ -479,7 +531,6 @@ def agentic_chat(
479
531
  display=display,
480
532
  index_name=index,
481
533
  agent_type=agent_type,
482
- account_id=account_id,
483
534
  collection_ids=collection_ids,
484
535
  verbose=verbose,
485
536
  )
@@ -500,6 +551,360 @@ def agentic_status() -> None:
500
551
  asyncio.run(agentic_status_command(display=display))
501
552
 
502
553
 
554
+ # ============================================================================
555
+ # MEMORY SUBCOMMAND GROUP
556
+ # ============================================================================
557
+
558
+ memory_app = typer.Typer(
559
+ name="memory",
560
+ help="Agentic Memory management commands.",
561
+ no_args_is_help=True,
562
+ rich_markup_mode="rich",
563
+ )
564
+ app.add_typer(memory_app, name="memory")
565
+
566
+ # Container sub-subcommand
567
+ container_app = typer.Typer(
568
+ name="container",
569
+ help="Memory container management.",
570
+ no_args_is_help=True,
571
+ rich_markup_mode="rich",
572
+ )
573
+ memory_app.add_typer(container_app, name="container")
574
+
575
+ # Session sub-subcommand
576
+ session_app = typer.Typer(
577
+ name="session",
578
+ help="Session management.",
579
+ no_args_is_help=True,
580
+ rich_markup_mode="rich",
581
+ )
582
+ memory_app.add_typer(session_app, name="session")
583
+
584
+
585
+ @memory_app.command("setup")
586
+ def memory_setup(
587
+ openai_key: Annotated[
588
+ Optional[str],
589
+ typer.Option("--openai-key", envvar="OPENAI_API_KEY", help="OpenAI API key for connector setup."),
590
+ ] = None,
591
+ llm_model: Annotated[
592
+ str,
593
+ typer.Option("--llm-model", help="LLM model for fact extraction."),
594
+ ] = "gpt-4o",
595
+ embedding_model: Annotated[
596
+ str,
597
+ typer.Option("--embedding-model", help="Embedding model name."),
598
+ ] = "text-embedding-3-small",
599
+ ) -> None:
600
+ """Setup OpenSearch for Agentic Memory.
601
+
602
+ Creates the required LLM and embedding connectors and models
603
+ for Agentic Memory to work.
604
+
605
+ [bold]Example:[/bold]
606
+ $ gnosisllm-knowledge memory setup --openai-key sk-...
607
+ $ gnosisllm-knowledge memory setup --llm-model gpt-4o --embedding-model text-embedding-3-small
608
+ """
609
+ from gnosisllm_knowledge.cli.commands.memory import memory_setup_command
610
+
611
+ asyncio.run(
612
+ memory_setup_command(
613
+ display=display,
614
+ openai_key=openai_key,
615
+ llm_model=llm_model,
616
+ embedding_model=embedding_model,
617
+ )
618
+ )
619
+
620
+
621
+ @memory_app.command("status")
622
+ def memory_status() -> None:
623
+ """Show memory configuration status.
624
+
625
+ Displays configured models and verifies their health.
626
+
627
+ [bold]Example:[/bold]
628
+ $ gnosisllm-knowledge memory status
629
+ """
630
+ from gnosisllm_knowledge.cli.commands.memory import memory_status_command
631
+
632
+ asyncio.run(memory_status_command(display=display))
633
+
634
+
635
+ @memory_app.command("store")
636
+ def memory_store(
637
+ container_id: Annotated[
638
+ str,
639
+ typer.Argument(help="Container ID to store messages in."),
640
+ ],
641
+ file: Annotated[
642
+ Optional[str],
643
+ typer.Option("--file", "-f", help="JSON file with messages."),
644
+ ] = None,
645
+ user_id: Annotated[
646
+ Optional[str],
647
+ typer.Option("--user-id", help="User ID for namespace."),
648
+ ] = None,
649
+ session_id: Annotated[
650
+ Optional[str],
651
+ typer.Option("--session-id", help="Session ID for namespace."),
652
+ ] = None,
653
+ infer: Annotated[
654
+ bool,
655
+ typer.Option("--infer/--no-infer", help="Enable/disable fact extraction."),
656
+ ] = True,
657
+ json_output: Annotated[
658
+ bool,
659
+ typer.Option("--json", "-j", help="Output as JSON."),
660
+ ] = False,
661
+ ) -> None:
662
+ """Store conversation in memory.
663
+
664
+ Stores messages in working memory and optionally extracts facts
665
+ to long-term memory using LLM inference.
666
+
667
+ [bold]Example:[/bold]
668
+ $ gnosisllm-knowledge memory store <container-id> -f messages.json --user-id alice
669
+ $ gnosisllm-knowledge memory store <container-id> -f messages.json --no-infer
670
+ """
671
+ from gnosisllm_knowledge.cli.commands.memory import memory_store_command
672
+
673
+ asyncio.run(
674
+ memory_store_command(
675
+ display=display,
676
+ container_id=container_id,
677
+ file=file,
678
+ user_id=user_id,
679
+ session_id=session_id,
680
+ infer=infer,
681
+ json_output=json_output,
682
+ )
683
+ )
684
+
685
+
686
+ @memory_app.command("recall")
687
+ def memory_recall(
688
+ container_id: Annotated[
689
+ str,
690
+ typer.Argument(help="Container ID to search."),
691
+ ],
692
+ query: Annotated[
693
+ str,
694
+ typer.Argument(help="Search query text."),
695
+ ],
696
+ user_id: Annotated[
697
+ Optional[str],
698
+ typer.Option("--user-id", help="Filter by user ID."),
699
+ ] = None,
700
+ session_id: Annotated[
701
+ Optional[str],
702
+ typer.Option("--session-id", help="Filter by session ID."),
703
+ ] = None,
704
+ limit: Annotated[
705
+ int,
706
+ typer.Option("--limit", "-n", help="Maximum results."),
707
+ ] = 10,
708
+ json_output: Annotated[
709
+ bool,
710
+ typer.Option("--json", "-j", help="Output as JSON."),
711
+ ] = False,
712
+ ) -> None:
713
+ """Search long-term memory.
714
+
715
+ Performs semantic search over extracted facts and memories.
716
+
717
+ [bold]Example:[/bold]
718
+ $ gnosisllm-knowledge memory recall <container-id> "user preferences" --user-id alice
719
+ $ gnosisllm-knowledge memory recall <container-id> "food preferences" --limit 5 --json
720
+ """
721
+ from gnosisllm_knowledge.cli.commands.memory import memory_recall_command
722
+
723
+ asyncio.run(
724
+ memory_recall_command(
725
+ display=display,
726
+ container_id=container_id,
727
+ query=query,
728
+ user_id=user_id,
729
+ session_id=session_id,
730
+ limit=limit,
731
+ json_output=json_output,
732
+ )
733
+ )
734
+
735
+
736
+ @memory_app.command("stats")
737
+ def memory_stats(
738
+ container_id: Annotated[
739
+ str,
740
+ typer.Argument(help="Container ID."),
741
+ ],
742
+ json_output: Annotated[
743
+ bool,
744
+ typer.Option("--json", "-j", help="Output as JSON."),
745
+ ] = False,
746
+ ) -> None:
747
+ """Show container statistics.
748
+
749
+ Displays memory counts, session count, and strategy breakdown.
750
+
751
+ [bold]Example:[/bold]
752
+ $ gnosisllm-knowledge memory stats <container-id>
753
+ $ gnosisllm-knowledge memory stats <container-id> --json
754
+ """
755
+ from gnosisllm_knowledge.cli.commands.memory import memory_stats_command
756
+
757
+ asyncio.run(
758
+ memory_stats_command(
759
+ display=display,
760
+ container_id=container_id,
761
+ json_output=json_output,
762
+ )
763
+ )
764
+
765
+
766
+ # === Container Commands ===
767
+
768
+
769
+ @container_app.command("create")
770
+ def container_create(
771
+ name: Annotated[
772
+ str,
773
+ typer.Argument(help="Container name."),
774
+ ],
775
+ description: Annotated[
776
+ Optional[str],
777
+ typer.Option("--description", "-d", help="Container description."),
778
+ ] = None,
779
+ config_file: Annotated[
780
+ Optional[str],
781
+ typer.Option("--config", "-c", help="JSON file with strategy configuration."),
782
+ ] = None,
783
+ ) -> None:
784
+ """Create a new memory container.
785
+
786
+ Containers hold memories with configurable extraction strategies.
787
+ Each strategy is scoped to namespace fields for partitioning.
788
+
789
+ [bold]Example config.json:[/bold]
790
+ {
791
+ "strategies": [
792
+ {"type": "SEMANTIC", "namespace": ["user_id"]},
793
+ {"type": "USER_PREFERENCE", "namespace": ["user_id"]},
794
+ {"type": "SUMMARY", "namespace": ["session_id"]}
795
+ ]
796
+ }
797
+
798
+ [bold]Example:[/bold]
799
+ $ gnosisllm-knowledge memory container create my-memory
800
+ $ gnosisllm-knowledge memory container create agent-memory -c config.json
801
+ """
802
+ from gnosisllm_knowledge.cli.commands.memory import container_create_command
803
+
804
+ asyncio.run(
805
+ container_create_command(
806
+ display=display,
807
+ name=name,
808
+ description=description,
809
+ config_file=config_file,
810
+ )
811
+ )
812
+
813
+
814
+ @container_app.command("list")
815
+ def container_list(
816
+ json_output: Annotated[
817
+ bool,
818
+ typer.Option("--json", "-j", help="Output as JSON."),
819
+ ] = False,
820
+ ) -> None:
821
+ """List all memory containers.
822
+
823
+ [bold]Example:[/bold]
824
+ $ gnosisllm-knowledge memory container list
825
+ $ gnosisllm-knowledge memory container list --json
826
+ """
827
+ from gnosisllm_knowledge.cli.commands.memory import container_list_command
828
+
829
+ asyncio.run(
830
+ container_list_command(
831
+ display=display,
832
+ json_output=json_output,
833
+ )
834
+ )
835
+
836
+
837
+ @container_app.command("delete")
838
+ def container_delete(
839
+ container_id: Annotated[
840
+ str,
841
+ typer.Argument(help="Container ID to delete."),
842
+ ],
843
+ force: Annotated[
844
+ bool,
845
+ typer.Option("--force", "-f", help="Skip confirmation prompt."),
846
+ ] = False,
847
+ ) -> None:
848
+ """Delete a memory container.
849
+
850
+ This permanently deletes the container and all its memories.
851
+
852
+ [bold]Example:[/bold]
853
+ $ gnosisllm-knowledge memory container delete <container-id>
854
+ $ gnosisllm-knowledge memory container delete <container-id> --force
855
+ """
856
+ from gnosisllm_knowledge.cli.commands.memory import container_delete_command
857
+
858
+ asyncio.run(
859
+ container_delete_command(
860
+ display=display,
861
+ container_id=container_id,
862
+ force=force,
863
+ )
864
+ )
865
+
866
+
867
+ # === Session Commands ===
868
+
869
+
870
+ @session_app.command("list")
871
+ def session_list(
872
+ container_id: Annotated[
873
+ str,
874
+ typer.Argument(help="Container ID."),
875
+ ],
876
+ user_id: Annotated[
877
+ Optional[str],
878
+ typer.Option("--user-id", help="Filter by user ID."),
879
+ ] = None,
880
+ limit: Annotated[
881
+ int,
882
+ typer.Option("--limit", "-n", help="Maximum sessions."),
883
+ ] = 20,
884
+ json_output: Annotated[
885
+ bool,
886
+ typer.Option("--json", "-j", help="Output as JSON."),
887
+ ] = False,
888
+ ) -> None:
889
+ """List sessions in a container.
890
+
891
+ [bold]Example:[/bold]
892
+ $ gnosisllm-knowledge memory session list <container-id>
893
+ $ gnosisllm-knowledge memory session list <container-id> --user-id alice
894
+ """
895
+ from gnosisllm_knowledge.cli.commands.memory import session_list_command
896
+
897
+ asyncio.run(
898
+ session_list_command(
899
+ display=display,
900
+ container_id=container_id,
901
+ user_id=user_id,
902
+ limit=limit,
903
+ json_output=json_output,
904
+ )
905
+ )
906
+
907
+
503
908
  def main() -> None:
504
909
  """CLI entry point."""
505
910
  app()
@@ -4,6 +4,10 @@ Commands:
4
4
  - setup: Configure agents in OpenSearch
5
5
  - chat: Interactive agentic chat session
6
6
  - status: Show agent configuration status
7
+
8
+ Note:
9
+ This library is tenant-agnostic. Multi-tenancy is achieved through index
10
+ isolation - each tenant should use a separate index (e.g., "knowledge-{account_id}").
7
11
  """
8
12
 
9
13
  from __future__ import annotations
@@ -105,6 +109,17 @@ async def agentic_setup_command(
105
109
  if agent_type in ("conversational", "all"):
106
110
  agent_types_to_setup.append("conversational")
107
111
 
112
+ # If force, cleanup existing agents first
113
+ if force:
114
+ display.info("Force mode: cleaning up existing agents...")
115
+ try:
116
+ cleanup_result = await adapter.cleanup_agents()
117
+ for step in cleanup_result.steps_completed:
118
+ display.success(step)
119
+ except Exception as e:
120
+ display.warning(f"Cleanup warning (continuing): {e}")
121
+ display.newline()
122
+
108
123
  # Build step list
109
124
  steps = []
110
125
  if "flow" in agent_types_to_setup:
@@ -191,17 +206,19 @@ async def agentic_chat_command(
191
206
  display: RichDisplayService,
192
207
  index_name: str = "knowledge",
193
208
  agent_type: str = "conversational",
194
- account_id: str | None = None,
195
209
  collection_ids: str | None = None,
196
210
  verbose: bool = False,
197
211
  ) -> None:
198
212
  """Interactive agentic chat session.
199
213
 
214
+ Note:
215
+ Multi-tenancy is achieved through index isolation. Use tenant-specific
216
+ index names instead (e.g., --index knowledge-tenant-123).
217
+
200
218
  Args:
201
219
  display: Display service for output.
202
- index_name: Index to search.
220
+ index_name: Index to search (use tenant-specific name for isolation).
203
221
  agent_type: Agent type ('flow' or 'conversational').
204
- account_id: Filter by account ID.
205
222
  collection_ids: Filter by collection IDs (comma-separated).
206
223
  verbose: Show reasoning steps.
207
224
  """
@@ -231,7 +248,6 @@ async def agentic_chat_command(
231
248
  if agent_type == "conversational":
232
249
  return await searcher.create_conversation(
233
250
  name="CLI Chat Session",
234
- account_id=account_id,
235
251
  )
236
252
  return None
237
253
 
@@ -280,7 +296,6 @@ async def agentic_chat_command(
280
296
  agent_type=AgentType.CONVERSATIONAL if agent_type == "conversational" else AgentType.FLOW,
281
297
  conversation_id=conversation_id,
282
298
  collection_ids=collection_list,
283
- account_id=account_id,
284
299
  include_reasoning=verbose,
285
300
  )
286
301
 
@@ -384,7 +399,6 @@ async def agentic_search_command(
384
399
  query: str,
385
400
  index_name: str = "knowledge",
386
401
  agent_type: str = "flow",
387
- account_id: str | None = None,
388
402
  collection_ids: str | None = None,
389
403
  source_ids: str | None = None,
390
404
  limit: int = 5,
@@ -393,12 +407,15 @@ async def agentic_search_command(
393
407
  ) -> dict[str, Any] | None:
394
408
  """Execute agentic search.
395
409
 
410
+ Note:
411
+ Multi-tenancy is achieved through index isolation. Use tenant-specific
412
+ index names instead (e.g., --index knowledge-tenant-123).
413
+
396
414
  Args:
397
415
  display: Display service for output.
398
416
  query: Search query text.
399
- index_name: Index to search.
417
+ index_name: Index to search (use tenant-specific name for isolation).
400
418
  agent_type: Agent type ('flow' or 'conversational').
401
- account_id: Filter by account ID.
402
419
  collection_ids: Filter by collection IDs (comma-separated).
403
420
  source_ids: Filter by source IDs (comma-separated).
404
421
  limit: Maximum source documents to retrieve.
@@ -436,12 +453,12 @@ async def agentic_search_command(
436
453
  )
437
454
 
438
455
  # Build query
456
+ # Note: account_id is deprecated and ignored - use index isolation instead
439
457
  agentic_query = AgenticSearchQuery(
440
458
  text=query,
441
459
  agent_type=AgentType.CONVERSATIONAL if agent_type == "conversational" else AgentType.FLOW,
442
460
  collection_ids=collection_list,
443
461
  source_ids=source_list,
444
- account_id=account_id,
445
462
  limit=limit,
446
463
  include_reasoning=verbose,
447
464
  )