kash-shell 0.3.24__py3-none-any.whl → 0.3.26__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.
- kash/commands/help/assistant_commands.py +4 -3
- kash/config/colors.py +5 -3
- kash/config/text_styles.py +1 -0
- kash/config/unified_live.py +251 -0
- kash/docs/markdown/assistant_instructions_template.md +3 -3
- kash/docs/markdown/topics/a1_what_is_kash.md +22 -20
- kash/docs/markdown/topics/a2_installation.md +10 -10
- kash/docs/markdown/topics/a3_getting_started.md +8 -8
- kash/docs/markdown/topics/a4_elements.md +3 -3
- kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +12 -12
- kash/docs/markdown/topics/b0_philosophy_of_kash.md +17 -17
- kash/docs/markdown/topics/b1_kash_overview.md +7 -7
- kash/docs/markdown/topics/b2_workspace_and_file_formats.md +1 -1
- kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +1 -1
- kash/docs/markdown/topics/b4_faq.md +7 -7
- kash/docs/markdown/welcome.md +1 -1
- kash/embeddings/embeddings.py +112 -43
- kash/embeddings/text_similarity.py +4 -7
- kash/exec/shell_callable_action.py +4 -3
- kash/help/help_embeddings.py +8 -2
- kash/llm_utils/llm_features.py +1 -1
- kash/llm_utils/llms.py +5 -7
- kash/model/graph_model.py +2 -0
- kash/model/items_model.py +3 -3
- kash/model/params_model.py +1 -1
- kash/shell/output/shell_output.py +2 -2
- kash/utils/file_utils/csv_utils.py +105 -0
- kash/utils/rich_custom/multitask_status.py +19 -5
- kash/utils/text_handling/doc_normalization.py +2 -0
- kash/web_gen/templates/base_styles.css.jinja +356 -24
- kash/web_gen/templates/base_webpage.html.jinja +11 -0
- kash/web_gen/templates/components/toc_styles.css.jinja +15 -3
- kash/web_gen/templates/components/tooltip_styles.css.jinja +1 -0
- kash/web_gen/templates/content_styles.css.jinja +23 -9
- kash/web_gen/templates/item_view.html.jinja +12 -4
- kash/web_gen/templates/simple_webpage.html.jinja +2 -2
- kash/xonsh_custom/custom_shell.py +7 -4
- {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/METADATA +58 -55
- {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/RECORD +42 -40
- {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.24.dist-info → kash_shell-0.3.26.dist-info}/licenses/LICENSE +0 -0
kash/model/params_model.py
CHANGED
|
@@ -209,7 +209,7 @@ A list of parameter declarations, possibly with default values.
|
|
|
209
209
|
DEFAULT_CAREFUL_LLM = LLM.o3
|
|
210
210
|
DEFAULT_STRUCTURED_LLM = LLM.gpt_4o
|
|
211
211
|
DEFAULT_STANDARD_LLM = LLM.claude_4_sonnet
|
|
212
|
-
DEFAULT_FAST_LLM = LLM.
|
|
212
|
+
DEFAULT_FAST_LLM = LLM.gpt_4o
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
# Parameters set globally such as in the workspace.
|
|
@@ -86,8 +86,8 @@ def multitask_status(
|
|
|
86
86
|
) -> MultiTaskStatus | nullcontext:
|
|
87
87
|
"""
|
|
88
88
|
Create a `MultiTaskStatus` context manager for displaying multiple task progress
|
|
89
|
-
using the global shell console
|
|
90
|
-
to disable status display.
|
|
89
|
+
using the global shell console with live display conflict prevention. If disabled,
|
|
90
|
+
returns a null context, so it's convenient to disable status display.
|
|
91
91
|
"""
|
|
92
92
|
if not enabled:
|
|
93
93
|
return nullcontext()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CsvMetadata(NamedTuple):
|
|
9
|
+
"""
|
|
10
|
+
Result of CSV analysis, containing skip rows, metadata, and dialect.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
skip_rows: int
|
|
14
|
+
metadata: dict[str, str]
|
|
15
|
+
dialect: type[csv.Dialect]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def sniff_csv_metadata(
|
|
19
|
+
file_path: Path,
|
|
20
|
+
*,
|
|
21
|
+
max_scan_lines: int = 500,
|
|
22
|
+
threshold_ratio: float = 0.8,
|
|
23
|
+
min_columns: int = 3,
|
|
24
|
+
sample_size: int = 32768,
|
|
25
|
+
) -> CsvMetadata:
|
|
26
|
+
"""
|
|
27
|
+
Detect CSV metadata and where the table data starts by finding the first row that looks
|
|
28
|
+
like proper headers.
|
|
29
|
+
|
|
30
|
+
This function handles various CSV formats:
|
|
31
|
+
- Normal CSV files: returns skip_rows=0 (no rows to skip)
|
|
32
|
+
- Files with metadata: detects the first row with multiple columns that looks like headers
|
|
33
|
+
- Survey exports: handles key-value metadata followed by proper CSV structure
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
file_path: Path to the CSV file to analyze
|
|
37
|
+
max_scan_lines: Maximum number of lines to scan before giving up
|
|
38
|
+
threshold_ratio: Minimum ratio of max columns a row must have to be considered headers
|
|
39
|
+
min_columns: Minimum number of columns required to be considered headers
|
|
40
|
+
sample_size: Number of bytes to read for dialect detection
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
CsvMetadata with skip_rows, metadata dict, and detected dialect
|
|
44
|
+
"""
|
|
45
|
+
# Read sample for dialect detection
|
|
46
|
+
sample_text = file_path.read_text(encoding="utf-8", errors="replace")[:sample_size]
|
|
47
|
+
|
|
48
|
+
# Detect CSV dialect
|
|
49
|
+
try:
|
|
50
|
+
dialect = csv.Sniffer().sniff(sample_text)
|
|
51
|
+
except csv.Error:
|
|
52
|
+
# Fall back to default dialect if detection fails
|
|
53
|
+
dialect = csv.excel
|
|
54
|
+
|
|
55
|
+
# Analyze file structure
|
|
56
|
+
with open(file_path, encoding="utf-8", errors="replace") as file:
|
|
57
|
+
reader = csv.reader(file, dialect=dialect)
|
|
58
|
+
|
|
59
|
+
max_columns = 0
|
|
60
|
+
header_candidates = []
|
|
61
|
+
metadata = {}
|
|
62
|
+
|
|
63
|
+
for line_num, row in enumerate(reader):
|
|
64
|
+
# Stop scanning if we've looked at too many lines
|
|
65
|
+
if line_num >= max_scan_lines:
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
# Skip completely empty rows
|
|
69
|
+
non_empty_cells = [cell.strip() for cell in row if cell.strip()]
|
|
70
|
+
if not non_empty_cells:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
column_count = len(non_empty_cells)
|
|
74
|
+
|
|
75
|
+
# Track the maximum number of columns seen
|
|
76
|
+
if column_count > max_columns:
|
|
77
|
+
max_columns = column_count
|
|
78
|
+
|
|
79
|
+
# Collect potential key-value metadata (exactly 2 columns)
|
|
80
|
+
# Only collect metadata before we find any header candidates with min_columns
|
|
81
|
+
if column_count == 2 and not any(hc[1] >= min_columns for hc in header_candidates):
|
|
82
|
+
key, value = non_empty_cells[0], non_empty_cells[1]
|
|
83
|
+
# Simple heuristic: if it looks like a key-value pair, store it
|
|
84
|
+
if not key.isdigit() and not value.replace(".", "").replace(",", "").isdigit():
|
|
85
|
+
metadata[key] = value
|
|
86
|
+
|
|
87
|
+
# Consider this a potential header if it has minimum required columns
|
|
88
|
+
if column_count >= min_columns:
|
|
89
|
+
header_candidates.append((line_num, column_count, row))
|
|
90
|
+
|
|
91
|
+
# If no multi-column rows found, assume it's a normal CSV starting at line 0
|
|
92
|
+
if not header_candidates:
|
|
93
|
+
return CsvMetadata(skip_rows=0, metadata=metadata, dialect=dialect)
|
|
94
|
+
|
|
95
|
+
# Look for the first row that has close to the maximum number of columns
|
|
96
|
+
# This helps distinguish metadata (usually fewer columns) from real headers (many columns)
|
|
97
|
+
threshold = max(min_columns, max_columns * threshold_ratio)
|
|
98
|
+
|
|
99
|
+
for line_num, column_count, _row in header_candidates:
|
|
100
|
+
if column_count >= threshold:
|
|
101
|
+
return CsvMetadata(skip_rows=line_num, metadata=metadata, dialect=dialect)
|
|
102
|
+
|
|
103
|
+
# If no clear header found but we have candidates, return the first multi-column row
|
|
104
|
+
first_candidate_line = header_candidates[0][0]
|
|
105
|
+
return CsvMetadata(skip_rows=first_candidate_line, metadata=metadata, dialect=dialect)
|
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
from contextlib import AbstractAsyncContextManager
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from types import TracebackType
|
|
7
|
-
from typing import TYPE_CHECKING, TypeVar
|
|
7
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
8
8
|
|
|
9
9
|
from strif import abbrev_str, single_line
|
|
10
10
|
from typing_extensions import override
|
|
@@ -17,6 +17,7 @@ from rich.progress import BarColumn, Progress, ProgressColumn, Task, TaskID
|
|
|
17
17
|
from rich.spinner import Spinner
|
|
18
18
|
from rich.text import Text
|
|
19
19
|
|
|
20
|
+
from kash.config.unified_live import get_unified_live
|
|
20
21
|
from kash.utils.api_utils.progress_protocol import (
|
|
21
22
|
EMOJI_FAILURE,
|
|
22
23
|
EMOJI_RETRY,
|
|
@@ -229,7 +230,7 @@ class TruncatedLabelColumn(ProgressColumn):
|
|
|
229
230
|
def __init__(self, console_width: int):
|
|
230
231
|
super().__init__()
|
|
231
232
|
# Reserve half the console width for labels/status messages
|
|
232
|
-
self.max_label_width = console_width // 2
|
|
233
|
+
self.max_label_width: int = console_width // 2
|
|
233
234
|
|
|
234
235
|
@override
|
|
235
236
|
def render(self, task: Task) -> Text:
|
|
@@ -298,6 +299,9 @@ class MultiTaskStatus(AbstractAsyncContextManager):
|
|
|
298
299
|
self._next_id: int = 1
|
|
299
300
|
self._rich_task_ids: dict[int, TaskID] = {} # Map our IDs to Rich Progress IDs
|
|
300
301
|
|
|
302
|
+
# Unified live integration
|
|
303
|
+
self._unified_live: Any | None = None # Reference to the global unified live
|
|
304
|
+
|
|
301
305
|
# Calculate spinner width for consistent spacing
|
|
302
306
|
self._spinner_width = _get_spinner_width(SPINNER_NAME)
|
|
303
307
|
|
|
@@ -367,7 +371,13 @@ class MultiTaskStatus(AbstractAsyncContextManager):
|
|
|
367
371
|
@override
|
|
368
372
|
async def __aenter__(self) -> MultiTaskStatus:
|
|
369
373
|
"""Start the live display."""
|
|
370
|
-
|
|
374
|
+
# Try to integrate with unified live display
|
|
375
|
+
|
|
376
|
+
# Always integrate with unified live display (auto-initialized)
|
|
377
|
+
unified_live = get_unified_live()
|
|
378
|
+
self._unified_live = unified_live
|
|
379
|
+
# Register our progress display with the unified live
|
|
380
|
+
unified_live.set_multitask_display(self._progress)
|
|
371
381
|
return self
|
|
372
382
|
|
|
373
383
|
@override
|
|
@@ -378,9 +388,13 @@ class MultiTaskStatus(AbstractAsyncContextManager):
|
|
|
378
388
|
exc_tb: TracebackType | None,
|
|
379
389
|
) -> None:
|
|
380
390
|
"""Stop the live display and show automatic summary if enabled."""
|
|
381
|
-
|
|
391
|
+
# Always clean up unified live integration
|
|
392
|
+
if self._unified_live is not None:
|
|
393
|
+
# Remove our display from the unified live
|
|
394
|
+
self._unified_live.set_multitask_display(None)
|
|
395
|
+
self._unified_live = None
|
|
382
396
|
|
|
383
|
-
# Show automatic summary if enabled
|
|
397
|
+
# Show automatic summary if enabled (always print to console now)
|
|
384
398
|
if self.auto_summary:
|
|
385
399
|
summary = self.get_summary()
|
|
386
400
|
self.console.print(summary)
|
|
@@ -21,6 +21,7 @@ def normalize_formatting(
|
|
|
21
21
|
format: Format | None,
|
|
22
22
|
support_ansi: bool = True,
|
|
23
23
|
cleanups: bool = True,
|
|
24
|
+
smartquotes: bool = True,
|
|
24
25
|
) -> str:
|
|
25
26
|
"""
|
|
26
27
|
Normalize formatting. Currently only normalizes Markdown and leaves plaintext
|
|
@@ -35,6 +36,7 @@ def normalize_formatting(
|
|
|
35
36
|
text,
|
|
36
37
|
line_wrapper=line_wrap_by_sentence(len_fn=len_fn, is_markdown=True),
|
|
37
38
|
cleanups=cleanups,
|
|
39
|
+
smartquotes=smartquotes,
|
|
38
40
|
)
|
|
39
41
|
elif format == Format.plaintext:
|
|
40
42
|
# Consider plaintext a raw format and don't normalize.
|