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.
- gnosisllm_knowledge/__init__.py +152 -0
- gnosisllm_knowledge/api/__init__.py +5 -0
- gnosisllm_knowledge/api/knowledge.py +548 -0
- gnosisllm_knowledge/backends/__init__.py +26 -0
- gnosisllm_knowledge/backends/memory/__init__.py +9 -0
- gnosisllm_knowledge/backends/memory/indexer.py +384 -0
- gnosisllm_knowledge/backends/memory/searcher.py +516 -0
- gnosisllm_knowledge/backends/opensearch/__init__.py +19 -0
- gnosisllm_knowledge/backends/opensearch/agentic.py +738 -0
- gnosisllm_knowledge/backends/opensearch/config.py +195 -0
- gnosisllm_knowledge/backends/opensearch/indexer.py +499 -0
- gnosisllm_knowledge/backends/opensearch/mappings.py +255 -0
- gnosisllm_knowledge/backends/opensearch/queries.py +445 -0
- gnosisllm_knowledge/backends/opensearch/searcher.py +383 -0
- gnosisllm_knowledge/backends/opensearch/setup.py +1390 -0
- gnosisllm_knowledge/chunking/__init__.py +9 -0
- gnosisllm_knowledge/chunking/fixed.py +138 -0
- gnosisllm_knowledge/chunking/sentence.py +239 -0
- gnosisllm_knowledge/cli/__init__.py +18 -0
- gnosisllm_knowledge/cli/app.py +509 -0
- gnosisllm_knowledge/cli/commands/__init__.py +7 -0
- gnosisllm_knowledge/cli/commands/agentic.py +529 -0
- gnosisllm_knowledge/cli/commands/load.py +369 -0
- gnosisllm_knowledge/cli/commands/search.py +440 -0
- gnosisllm_knowledge/cli/commands/setup.py +228 -0
- gnosisllm_knowledge/cli/display/__init__.py +5 -0
- gnosisllm_knowledge/cli/display/service.py +555 -0
- gnosisllm_knowledge/cli/utils/__init__.py +5 -0
- gnosisllm_knowledge/cli/utils/config.py +207 -0
- gnosisllm_knowledge/core/__init__.py +87 -0
- gnosisllm_knowledge/core/domain/__init__.py +43 -0
- gnosisllm_knowledge/core/domain/document.py +240 -0
- gnosisllm_knowledge/core/domain/result.py +176 -0
- gnosisllm_knowledge/core/domain/search.py +327 -0
- gnosisllm_knowledge/core/domain/source.py +139 -0
- gnosisllm_knowledge/core/events/__init__.py +23 -0
- gnosisllm_knowledge/core/events/emitter.py +216 -0
- gnosisllm_knowledge/core/events/types.py +226 -0
- gnosisllm_knowledge/core/exceptions.py +407 -0
- gnosisllm_knowledge/core/interfaces/__init__.py +20 -0
- gnosisllm_knowledge/core/interfaces/agentic.py +136 -0
- gnosisllm_knowledge/core/interfaces/chunker.py +64 -0
- gnosisllm_knowledge/core/interfaces/fetcher.py +112 -0
- gnosisllm_knowledge/core/interfaces/indexer.py +244 -0
- gnosisllm_knowledge/core/interfaces/loader.py +102 -0
- gnosisllm_knowledge/core/interfaces/searcher.py +178 -0
- gnosisllm_knowledge/core/interfaces/setup.py +164 -0
- gnosisllm_knowledge/fetchers/__init__.py +12 -0
- gnosisllm_knowledge/fetchers/config.py +77 -0
- gnosisllm_knowledge/fetchers/http.py +167 -0
- gnosisllm_knowledge/fetchers/neoreader.py +204 -0
- gnosisllm_knowledge/loaders/__init__.py +13 -0
- gnosisllm_knowledge/loaders/base.py +399 -0
- gnosisllm_knowledge/loaders/factory.py +202 -0
- gnosisllm_knowledge/loaders/sitemap.py +285 -0
- gnosisllm_knowledge/loaders/website.py +57 -0
- gnosisllm_knowledge/py.typed +0 -0
- gnosisllm_knowledge/services/__init__.py +9 -0
- gnosisllm_knowledge/services/indexing.py +387 -0
- gnosisllm_knowledge/services/search.py +349 -0
- gnosisllm_knowledge-0.2.0.dist-info/METADATA +382 -0
- gnosisllm_knowledge-0.2.0.dist-info/RECORD +64 -0
- gnosisllm_knowledge-0.2.0.dist-info/WHEEL +4 -0
- 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
|
+
)
|