gnosisllm-knowledge 0.2.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 (64) hide show
  1. gnosisllm_knowledge/__init__.py +152 -0
  2. gnosisllm_knowledge/api/__init__.py +5 -0
  3. gnosisllm_knowledge/api/knowledge.py +548 -0
  4. gnosisllm_knowledge/backends/__init__.py +26 -0
  5. gnosisllm_knowledge/backends/memory/__init__.py +9 -0
  6. gnosisllm_knowledge/backends/memory/indexer.py +384 -0
  7. gnosisllm_knowledge/backends/memory/searcher.py +516 -0
  8. gnosisllm_knowledge/backends/opensearch/__init__.py +19 -0
  9. gnosisllm_knowledge/backends/opensearch/agentic.py +738 -0
  10. gnosisllm_knowledge/backends/opensearch/config.py +195 -0
  11. gnosisllm_knowledge/backends/opensearch/indexer.py +499 -0
  12. gnosisllm_knowledge/backends/opensearch/mappings.py +255 -0
  13. gnosisllm_knowledge/backends/opensearch/queries.py +445 -0
  14. gnosisllm_knowledge/backends/opensearch/searcher.py +383 -0
  15. gnosisllm_knowledge/backends/opensearch/setup.py +1390 -0
  16. gnosisllm_knowledge/chunking/__init__.py +9 -0
  17. gnosisllm_knowledge/chunking/fixed.py +138 -0
  18. gnosisllm_knowledge/chunking/sentence.py +239 -0
  19. gnosisllm_knowledge/cli/__init__.py +18 -0
  20. gnosisllm_knowledge/cli/app.py +509 -0
  21. gnosisllm_knowledge/cli/commands/__init__.py +7 -0
  22. gnosisllm_knowledge/cli/commands/agentic.py +529 -0
  23. gnosisllm_knowledge/cli/commands/load.py +369 -0
  24. gnosisllm_knowledge/cli/commands/search.py +440 -0
  25. gnosisllm_knowledge/cli/commands/setup.py +228 -0
  26. gnosisllm_knowledge/cli/display/__init__.py +5 -0
  27. gnosisllm_knowledge/cli/display/service.py +555 -0
  28. gnosisllm_knowledge/cli/utils/__init__.py +5 -0
  29. gnosisllm_knowledge/cli/utils/config.py +207 -0
  30. gnosisllm_knowledge/core/__init__.py +87 -0
  31. gnosisllm_knowledge/core/domain/__init__.py +43 -0
  32. gnosisllm_knowledge/core/domain/document.py +240 -0
  33. gnosisllm_knowledge/core/domain/result.py +176 -0
  34. gnosisllm_knowledge/core/domain/search.py +327 -0
  35. gnosisllm_knowledge/core/domain/source.py +139 -0
  36. gnosisllm_knowledge/core/events/__init__.py +23 -0
  37. gnosisllm_knowledge/core/events/emitter.py +216 -0
  38. gnosisllm_knowledge/core/events/types.py +226 -0
  39. gnosisllm_knowledge/core/exceptions.py +407 -0
  40. gnosisllm_knowledge/core/interfaces/__init__.py +20 -0
  41. gnosisllm_knowledge/core/interfaces/agentic.py +136 -0
  42. gnosisllm_knowledge/core/interfaces/chunker.py +64 -0
  43. gnosisllm_knowledge/core/interfaces/fetcher.py +112 -0
  44. gnosisllm_knowledge/core/interfaces/indexer.py +244 -0
  45. gnosisllm_knowledge/core/interfaces/loader.py +102 -0
  46. gnosisllm_knowledge/core/interfaces/searcher.py +178 -0
  47. gnosisllm_knowledge/core/interfaces/setup.py +164 -0
  48. gnosisllm_knowledge/fetchers/__init__.py +12 -0
  49. gnosisllm_knowledge/fetchers/config.py +77 -0
  50. gnosisllm_knowledge/fetchers/http.py +167 -0
  51. gnosisllm_knowledge/fetchers/neoreader.py +204 -0
  52. gnosisllm_knowledge/loaders/__init__.py +13 -0
  53. gnosisllm_knowledge/loaders/base.py +399 -0
  54. gnosisllm_knowledge/loaders/factory.py +202 -0
  55. gnosisllm_knowledge/loaders/sitemap.py +285 -0
  56. gnosisllm_knowledge/loaders/website.py +57 -0
  57. gnosisllm_knowledge/py.typed +0 -0
  58. gnosisllm_knowledge/services/__init__.py +9 -0
  59. gnosisllm_knowledge/services/indexing.py +387 -0
  60. gnosisllm_knowledge/services/search.py +349 -0
  61. gnosisllm_knowledge-0.2.0.dist-info/METADATA +382 -0
  62. gnosisllm_knowledge-0.2.0.dist-info/RECORD +64 -0
  63. gnosisllm_knowledge-0.2.0.dist-info/WHEEL +4 -0
  64. gnosisllm_knowledge-0.2.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,555 @@
1
+ """Rich-based display service for enterprise-grade CLI output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Protocol
7
+
8
+ from rich import box
9
+ from rich.console import Console
10
+ from rich.live import Live
11
+ from rich.panel import Panel
12
+ from rich.progress import (
13
+ BarColumn,
14
+ MofNCompleteColumn,
15
+ Progress,
16
+ SpinnerColumn,
17
+ TaskProgressColumn,
18
+ TextColumn,
19
+ TimeElapsedColumn,
20
+ )
21
+ from rich.prompt import Confirm, Prompt
22
+ from rich.table import Table
23
+ from rich.text import Text
24
+
25
+
26
+ @dataclass
27
+ class StepProgress:
28
+ """Represents a setup step with status tracking."""
29
+
30
+ name: str
31
+ description: str
32
+ status: str = "pending" # pending, running, success, failed, skipped
33
+
34
+
35
+ class ProgressContext(Protocol):
36
+ """Protocol for multi-step progress tracking."""
37
+
38
+ def update(self, step: int, status: str) -> None:
39
+ """Update step status."""
40
+ ...
41
+
42
+ def complete(self, step: int) -> None:
43
+ """Mark step as completed."""
44
+ ...
45
+
46
+ def fail(self, step: int, error: str) -> None:
47
+ """Mark step as failed with error message."""
48
+ ...
49
+
50
+ def skip(self, step: int, reason: str) -> None:
51
+ """Mark step as skipped with reason."""
52
+ ...
53
+
54
+ def stop(self) -> None:
55
+ """Stop the progress display."""
56
+ ...
57
+
58
+
59
+ class RichStepProgress:
60
+ """Rich-based multi-step progress display using Live updates."""
61
+
62
+ STATUS_ICONS = {
63
+ "pending": "[dim]○[/dim]",
64
+ "running": "[yellow]◐[/yellow]",
65
+ "success": "[green]✓[/green]",
66
+ "failed": "[red]✗[/red]",
67
+ "skipped": "[dim]⊘[/dim]",
68
+ }
69
+
70
+ def __init__(self, console: Console, steps: list[StepProgress]) -> None:
71
+ """Initialize progress display.
72
+
73
+ Args:
74
+ console: Rich console instance.
75
+ steps: List of steps to track.
76
+ """
77
+ self._console = console
78
+ self._steps = steps
79
+ self._live = Live(self._build_table(), console=console, refresh_per_second=4)
80
+ self._live.start()
81
+
82
+ def _build_table(self) -> Table:
83
+ """Build the progress table."""
84
+ table = Table(box=box.ROUNDED, show_header=False, padding=(0, 1))
85
+ table.add_column("Status", width=3)
86
+ table.add_column("Step", style="cyan", width=20)
87
+ table.add_column("Description")
88
+
89
+ for step in self._steps:
90
+ icon = self.STATUS_ICONS.get(step.status, "○")
91
+ style = "dim" if step.status in ("pending", "skipped") else ""
92
+ table.add_row(icon, step.name, step.description, style=style)
93
+
94
+ return table
95
+
96
+ def _refresh(self) -> None:
97
+ """Refresh the live display."""
98
+ self._live.update(self._build_table())
99
+
100
+ def update(self, step: int, status: str) -> None:
101
+ """Update step status."""
102
+ if 0 <= step < len(self._steps):
103
+ self._steps[step].status = status
104
+ self._refresh()
105
+
106
+ def complete(self, step: int) -> None:
107
+ """Mark step as completed."""
108
+ self.update(step, "success")
109
+
110
+ def fail(self, step: int, error: str) -> None:
111
+ """Mark step as failed."""
112
+ if 0 <= step < len(self._steps):
113
+ self._steps[step].status = "failed"
114
+ original_desc = self._steps[step].description.split(" - ")[0]
115
+ self._steps[step].description = f"{original_desc} - {error}"
116
+ self._refresh()
117
+
118
+ def skip(self, step: int, reason: str) -> None:
119
+ """Mark step as skipped."""
120
+ if 0 <= step < len(self._steps):
121
+ self._steps[step].status = "skipped"
122
+ original_desc = self._steps[step].description.split(" (")[0]
123
+ self._steps[step].description = f"{original_desc} ({reason})"
124
+ self._refresh()
125
+
126
+ def stop(self) -> None:
127
+ """Stop the live display."""
128
+ self._live.stop()
129
+
130
+
131
+ @dataclass
132
+ class SearchResultDisplay:
133
+ """Data for displaying a search result."""
134
+
135
+ rank: int
136
+ title: str
137
+ content_preview: str
138
+ score: float
139
+ url: str | None = None
140
+ collection_id: str | None = None
141
+ highlights: list[str] = field(default_factory=list)
142
+
143
+
144
+ class RichDisplayService:
145
+ """Rich-based terminal display service for enterprise-grade CLI UX."""
146
+
147
+ BORDER_STYLES = {
148
+ "info": "blue",
149
+ "success": "green",
150
+ "warning": "yellow",
151
+ "error": "red",
152
+ }
153
+
154
+ def __init__(self, console: Console | None = None) -> None:
155
+ """Initialize display service.
156
+
157
+ Args:
158
+ console: Optional Rich console. Creates new if not provided.
159
+ """
160
+ self._console = console or Console()
161
+
162
+ @property
163
+ def console(self) -> Console:
164
+ """Get the underlying console."""
165
+ return self._console
166
+
167
+ def header(self, title: str, subtitle: str | None = None) -> None:
168
+ """Display a styled header.
169
+
170
+ Args:
171
+ title: Main title text.
172
+ subtitle: Optional subtitle.
173
+ """
174
+ self._console.print()
175
+ panel_content = f"[bold]{title}[/bold]"
176
+ if subtitle:
177
+ panel_content += f"\n[dim]{subtitle}[/dim]"
178
+
179
+ self._console.print(
180
+ Panel(
181
+ panel_content,
182
+ box=box.ROUNDED,
183
+ border_style="blue",
184
+ padding=(0, 2),
185
+ )
186
+ )
187
+ self._console.print()
188
+
189
+ def info(self, message: str) -> None:
190
+ """Display informational message."""
191
+ self._console.print(f"[blue]ℹ[/blue] {message}")
192
+
193
+ def success(self, message: str) -> None:
194
+ """Display success message."""
195
+ self._console.print(f"[green]✓[/green] {message}")
196
+
197
+ def error(self, message: str) -> None:
198
+ """Display error message."""
199
+ self._console.print(f"[red]✗[/red] {message}")
200
+
201
+ def warning(self, message: str) -> None:
202
+ """Display warning message."""
203
+ self._console.print(f"[yellow]⚠[/yellow] {message}")
204
+
205
+ def table(
206
+ self,
207
+ title: str,
208
+ rows: list[tuple[str, ...]],
209
+ headers: list[str] | None = None,
210
+ ) -> None:
211
+ """Display a formatted table.
212
+
213
+ Args:
214
+ title: Table title.
215
+ rows: List of row tuples.
216
+ headers: Optional column headers. Defaults to ["Setting", "Value"].
217
+ """
218
+ table = Table(title=title, box=box.ROUNDED)
219
+
220
+ if headers:
221
+ for i, header in enumerate(headers):
222
+ style = "cyan" if i == 0 else "green" if i == 1 else ""
223
+ table.add_column(header, style=style)
224
+ else:
225
+ table.add_column("Setting", style="cyan")
226
+ table.add_column("Value", style="green")
227
+
228
+ for row in rows:
229
+ table.add_row(*row)
230
+
231
+ self._console.print(table)
232
+
233
+ def progress(self, steps: list[StepProgress]) -> RichStepProgress:
234
+ """Create multi-step progress display.
235
+
236
+ Args:
237
+ steps: List of steps to track.
238
+
239
+ Returns:
240
+ Progress context for updating step status.
241
+ """
242
+ return RichStepProgress(self._console, steps)
243
+
244
+ def progress_bar(
245
+ self,
246
+ description: str = "Processing",
247
+ total: int | None = None,
248
+ ) -> Progress:
249
+ """Create a progress bar for batch operations.
250
+
251
+ Args:
252
+ description: Description text.
253
+ total: Total items (None for indeterminate).
254
+
255
+ Returns:
256
+ Rich Progress instance.
257
+ """
258
+ return Progress(
259
+ SpinnerColumn(),
260
+ TextColumn("[progress.description]{task.description}"),
261
+ BarColumn(),
262
+ TaskProgressColumn(),
263
+ MofNCompleteColumn(),
264
+ TimeElapsedColumn(),
265
+ console=self._console,
266
+ )
267
+
268
+ def confirm(self, message: str, default: bool = False) -> bool:
269
+ """Prompt for yes/no confirmation.
270
+
271
+ Args:
272
+ message: Confirmation message.
273
+ default: Default value if Enter is pressed.
274
+
275
+ Returns:
276
+ True if confirmed, False otherwise.
277
+ """
278
+ return Confirm.ask(message, default=default)
279
+
280
+ def prompt(self, message: str, default: str | None = None) -> str:
281
+ """Prompt for text input.
282
+
283
+ Args:
284
+ message: Prompt message.
285
+ default: Optional default value.
286
+
287
+ Returns:
288
+ User input string.
289
+ """
290
+ return Prompt.ask(message, default=default or "")
291
+
292
+ def panel(self, content: str, title: str, style: str = "info") -> None:
293
+ """Display a styled panel/box.
294
+
295
+ Args:
296
+ content: Panel content.
297
+ title: Panel title.
298
+ style: Style name (info, success, warning, error).
299
+ """
300
+ border_style = self.BORDER_STYLES.get(style, "blue")
301
+ self._console.print(Panel(content, title=title, border_style=border_style))
302
+
303
+ def newline(self) -> None:
304
+ """Print an empty line."""
305
+ self._console.print()
306
+
307
+ def rule(self, title: str = "") -> None:
308
+ """Print a horizontal rule.
309
+
310
+ Args:
311
+ title: Optional title in the rule.
312
+ """
313
+ self._console.rule(title)
314
+
315
+ def search_results(
316
+ self,
317
+ results: list[SearchResultDisplay],
318
+ query: str,
319
+ total_hits: int,
320
+ duration_ms: float,
321
+ mode: str,
322
+ ) -> None:
323
+ """Display search results in a beautiful format.
324
+
325
+ Args:
326
+ results: List of search results to display.
327
+ query: Original search query.
328
+ total_hits: Total number of hits.
329
+ duration_ms: Search duration in milliseconds.
330
+ mode: Search mode used.
331
+ """
332
+ # Analytics table
333
+ self.table(
334
+ "Search Analytics",
335
+ [
336
+ ("Query", query[:60] + "..." if len(query) > 60 else query),
337
+ ("Mode", mode),
338
+ ("Total Hits", str(total_hits)),
339
+ ("Duration", f"{duration_ms:.1f}ms"),
340
+ ],
341
+ )
342
+
343
+ self.newline()
344
+
345
+ if not results:
346
+ self.warning("No results found.")
347
+ return
348
+
349
+ # Results panel
350
+ result_text = Text()
351
+ for i, result in enumerate(results):
352
+ if i > 0:
353
+ result_text.append("\n\n")
354
+
355
+ # Title line with score
356
+ score_pct = result.score * 100 if result.score <= 1 else result.score
357
+ result_text.append(f"{result.rank}. ", style="bold cyan")
358
+ result_text.append(result.title or "Untitled", style="bold")
359
+ result_text.append(f" Score: {score_pct:.1f}%", style="dim green")
360
+ result_text.append("\n")
361
+
362
+ # Separator
363
+ result_text.append("─" * 60, style="dim")
364
+ result_text.append("\n")
365
+
366
+ # Content preview
367
+ result_text.append(result.content_preview)
368
+
369
+ # URL if available
370
+ if result.url:
371
+ result_text.append(f"\nURL: ", style="dim")
372
+ result_text.append(result.url, style="blue underline")
373
+
374
+ # Collection if available
375
+ if result.collection_id:
376
+ result_text.append(f"\nCollection: ", style="dim")
377
+ result_text.append(result.collection_id, style="magenta")
378
+
379
+ self._console.print(
380
+ Panel(
381
+ result_text,
382
+ title=f"Results (Top {len(results)})",
383
+ border_style="green",
384
+ padding=(1, 2),
385
+ )
386
+ )
387
+
388
+ def format_error_with_suggestion(
389
+ self,
390
+ error: str,
391
+ suggestion: str | None = None,
392
+ command: str | None = None,
393
+ ) -> None:
394
+ """Display an error with a helpful suggestion.
395
+
396
+ Args:
397
+ error: Error message.
398
+ suggestion: Optional suggestion text.
399
+ command: Optional command to run.
400
+ """
401
+ content = f"[red]{error}[/red]"
402
+ if suggestion:
403
+ content += f"\n\n[yellow]Suggestion:[/yellow] {suggestion}"
404
+ if command:
405
+ content += f"\n\n[dim]Run:[/dim] [cyan]{command}[/cyan]"
406
+
407
+ self._console.print(
408
+ Panel(content, title="Error", border_style="red", padding=(1, 2))
409
+ )
410
+
411
+ def loading_spinner(self, message: str) -> Any:
412
+ """Create a loading spinner context.
413
+
414
+ Args:
415
+ message: Loading message.
416
+
417
+ Returns:
418
+ Context manager for spinner.
419
+ """
420
+ return self._console.status(message, spinner="dots")
421
+
422
+ def json_output(self, data: dict[str, Any]) -> None:
423
+ """Output data as formatted JSON.
424
+
425
+ Args:
426
+ data: Dictionary to output as JSON.
427
+ """
428
+ import json
429
+
430
+ self._console.print(json.dumps(data, indent=2, default=str))
431
+
432
+ def agentic_result(
433
+ self,
434
+ answer: str | None,
435
+ sources: list[Any],
436
+ reasoning_steps: list[Any] | None = None,
437
+ duration_ms: float = 0.0,
438
+ query: str | None = None,
439
+ conversation_id: str | None = None,
440
+ verbose: bool = False,
441
+ ) -> None:
442
+ """Display agentic search result with answer and sources.
443
+
444
+ Args:
445
+ answer: AI-generated answer text.
446
+ sources: List of source documents (SearchResultItem).
447
+ reasoning_steps: Optional reasoning steps for verbose mode.
448
+ duration_ms: Search duration in milliseconds.
449
+ query: Original query for reference.
450
+ conversation_id: Conversation ID for multi-turn.
451
+ verbose: Show detailed reasoning steps.
452
+ """
453
+ # Answer panel
454
+ if answer:
455
+ answer_content = answer
456
+ if query:
457
+ answer_content = f"[dim]Query: {query[:60]}{'...' if len(query) > 60 else ''}[/dim]\n\n{answer}"
458
+
459
+ self._console.print(
460
+ Panel(
461
+ answer_content,
462
+ title=f"Answer ({duration_ms:.0f}ms)",
463
+ border_style="green",
464
+ padding=(1, 2),
465
+ )
466
+ )
467
+ else:
468
+ self.warning("No answer generated by the agent.")
469
+
470
+ self.newline()
471
+
472
+ # Reasoning steps (if verbose)
473
+ if verbose and reasoning_steps:
474
+ self._console.print("[bold]Reasoning Steps:[/bold]")
475
+ for i, step in enumerate(reasoning_steps, 1):
476
+ tool = getattr(step, "tool", "unknown")
477
+ action = getattr(step, "action", "")
478
+ output = getattr(step, "output", "")
479
+ self._console.print(f" {i}. [cyan]{tool}[/cyan] → {action}")
480
+ if output:
481
+ output_preview = output[:100] + "..." if len(output) > 100 else output
482
+ self._console.print(f" [dim]{output_preview}[/dim]")
483
+ self.newline()
484
+
485
+ # Sources
486
+ if sources:
487
+ self._console.print(f"[bold]Sources ({len(sources)}):[/bold]")
488
+ for i, item in enumerate(sources[:5], 1):
489
+ score = getattr(item, "score", 0.0)
490
+ score_pct = score * 100 if score <= 1 else score
491
+ title = getattr(item, "title", "Untitled") or "Untitled"
492
+ url = getattr(item, "url", None)
493
+
494
+ self._console.print(
495
+ f" {i}. [bold]{title}[/bold] "
496
+ f"[dim]({score_pct:.1f}%)[/dim]"
497
+ )
498
+ if url:
499
+ self._console.print(f" [blue]{url}[/blue]")
500
+
501
+ # Conversation info
502
+ if conversation_id:
503
+ self.newline()
504
+ self.info(f"[dim]Conversation ID: {conversation_id}[/dim]")
505
+
506
+ def agentic_status(
507
+ self,
508
+ flow_agent_id: str | None,
509
+ conversational_agent_id: str | None,
510
+ embedding_model_id: str | None,
511
+ llm_model: str = "gpt-4o",
512
+ ) -> None:
513
+ """Display agentic search configuration status.
514
+
515
+ Args:
516
+ flow_agent_id: Flow agent ID if configured.
517
+ conversational_agent_id: Conversational agent ID if configured.
518
+ embedding_model_id: Embedding model ID.
519
+ llm_model: LLM model name for reasoning.
520
+ """
521
+ status_rows = []
522
+
523
+ # Flow agent
524
+ if flow_agent_id:
525
+ status_rows.append(("Flow Agent", "[green]Configured[/green]"))
526
+ status_rows.append((" ID", f"[dim]{flow_agent_id}[/dim]"))
527
+ else:
528
+ status_rows.append(("Flow Agent", "[yellow]Not configured[/yellow]"))
529
+
530
+ # Conversational agent
531
+ if conversational_agent_id:
532
+ status_rows.append(("Conversational Agent", "[green]Configured[/green]"))
533
+ status_rows.append((" ID", f"[dim]{conversational_agent_id}[/dim]"))
534
+ else:
535
+ status_rows.append(("Conversational Agent", "[yellow]Not configured[/yellow]"))
536
+
537
+ # Embedding model
538
+ if embedding_model_id:
539
+ status_rows.append(("Embedding Model", "[green]Configured[/green]"))
540
+ status_rows.append((" ID", f"[dim]{embedding_model_id}[/dim]"))
541
+ else:
542
+ status_rows.append(("Embedding Model", "[red]Not configured[/red]"))
543
+
544
+ # LLM model
545
+ status_rows.append(("LLM Model", llm_model))
546
+
547
+ self.table("Agentic Search Configuration", status_rows)
548
+
549
+ if not flow_agent_id and not conversational_agent_id:
550
+ self.newline()
551
+ self.format_error_with_suggestion(
552
+ error="No agents configured.",
553
+ suggestion="Run agentic setup to create agents.",
554
+ command="gnosisllm-knowledge agentic setup",
555
+ )
@@ -0,0 +1,5 @@
1
+ """CLI utilities."""
2
+
3
+ from gnosisllm_knowledge.cli.utils.config import CliConfig
4
+
5
+ __all__ = ["CliConfig"]