kash-shell 0.3.28__py3-none-any.whl → 0.3.30__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/actions/core/markdownify_html.py +1 -4
- kash/actions/core/minify_html.py +4 -5
- kash/actions/core/render_as_html.py +9 -7
- kash/actions/core/save_sidematter_meta.py +47 -0
- kash/actions/core/zip_sidematter.py +47 -0
- kash/commands/base/basic_file_commands.py +7 -4
- kash/commands/base/diff_commands.py +6 -4
- kash/commands/base/files_command.py +31 -30
- kash/commands/base/general_commands.py +3 -2
- kash/commands/base/logs_commands.py +6 -4
- kash/commands/base/reformat_command.py +3 -2
- kash/commands/base/search_command.py +4 -3
- kash/commands/base/show_command.py +9 -7
- kash/commands/help/assistant_commands.py +6 -4
- kash/commands/help/help_commands.py +7 -4
- kash/commands/workspace/selection_commands.py +18 -16
- kash/commands/workspace/workspace_commands.py +39 -26
- kash/config/setup.py +2 -27
- kash/docs/markdown/topics/a1_what_is_kash.md +26 -18
- kash/exec/action_decorators.py +2 -2
- kash/exec/action_exec.py +56 -50
- kash/exec/fetch_url_items.py +36 -9
- kash/exec/preconditions.py +2 -2
- kash/exec/resolve_args.py +4 -1
- kash/exec/runtime_settings.py +1 -0
- kash/file_storage/file_store.py +59 -23
- kash/file_storage/item_file_format.py +91 -26
- kash/help/help_types.py +1 -1
- kash/llm_utils/llms.py +6 -1
- kash/local_server/local_server_commands.py +2 -1
- kash/mcp/mcp_server_commands.py +3 -2
- kash/mcp/mcp_server_routes.py +1 -1
- kash/model/actions_model.py +31 -30
- kash/model/compound_actions_model.py +4 -3
- kash/model/exec_model.py +30 -3
- kash/model/items_model.py +114 -57
- kash/model/params_model.py +4 -4
- kash/shell/output/shell_output.py +1 -2
- kash/utils/file_formats/chat_format.py +7 -4
- kash/utils/file_utils/file_ext.py +1 -0
- kash/utils/file_utils/file_formats.py +4 -2
- kash/utils/file_utils/file_formats_model.py +12 -0
- kash/utils/text_handling/doc_normalization.py +1 -1
- kash/utils/text_handling/markdown_footnotes.py +224 -0
- kash/utils/text_handling/markdown_utils.py +532 -41
- kash/utils/text_handling/markdownify_utils.py +2 -1
- kash/web_gen/templates/components/tooltip_scripts.js.jinja +186 -1
- kash/web_gen/templates/components/youtube_popover_scripts.js.jinja +223 -0
- kash/web_gen/templates/components/youtube_popover_styles.css.jinja +150 -0
- kash/web_gen/templates/content_styles.css.jinja +53 -1
- kash/web_gen/templates/youtube_webpage.html.jinja +47 -0
- kash/web_gen/webpage_render.py +103 -0
- kash/workspaces/workspaces.py +0 -5
- kash/xonsh_custom/custom_shell.py +4 -3
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/METADATA +33 -24
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/RECORD +59 -54
- kash/llm_utils/llm_features.py +0 -72
- kash/web_gen/simple_webpage.py +0 -55
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/licenses/LICENSE +0 -0
kash/model/items_model.py
CHANGED
|
@@ -17,7 +17,7 @@ from prettyfmt import (
|
|
|
17
17
|
slugify_snake,
|
|
18
18
|
)
|
|
19
19
|
from pydantic.dataclasses import dataclass
|
|
20
|
-
from strif import abbrev_str, format_iso_timestamp
|
|
20
|
+
from strif import abbrev_str, format_iso_timestamp, single_line
|
|
21
21
|
|
|
22
22
|
from kash.config.logger import get_logger
|
|
23
23
|
from kash.model.concept_model import canonicalize_concept
|
|
@@ -34,7 +34,9 @@ from kash.utils.text_handling.markdown_render import markdown_to_html
|
|
|
34
34
|
from kash.utils.text_handling.markdown_utils import first_heading
|
|
35
35
|
|
|
36
36
|
if TYPE_CHECKING:
|
|
37
|
-
from
|
|
37
|
+
from sidematter_format import ResolvedSidematter
|
|
38
|
+
|
|
39
|
+
from kash.model.exec_model import ActionContext
|
|
38
40
|
from kash.workspaces import Workspace
|
|
39
41
|
|
|
40
42
|
log = get_logger(__name__)
|
|
@@ -68,7 +70,15 @@ class ItemType(Enum):
|
|
|
68
70
|
"""
|
|
69
71
|
Resources don't have a body. On concepts it's optional.
|
|
70
72
|
"""
|
|
71
|
-
return self.value not in
|
|
73
|
+
return self.value not in (ItemType.resource.value, ItemType.concept.value)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def allows_op_suffix(self) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Whether it makes sense to have an operation suffix for this item type
|
|
79
|
+
(docs often should, but concepts or resources can have cleaner naming conventions).
|
|
80
|
+
"""
|
|
81
|
+
return self not in (ItemType.concept, ItemType.resource, ItemType.export)
|
|
72
82
|
|
|
73
83
|
@staticmethod
|
|
74
84
|
def for_format(format: Format) -> ItemType:
|
|
@@ -100,6 +110,7 @@ class ItemType(Enum):
|
|
|
100
110
|
Format.mp3: ItemType.resource,
|
|
101
111
|
Format.m4a: ItemType.resource,
|
|
102
112
|
Format.mp4: ItemType.resource,
|
|
113
|
+
Format.zip: ItemType.resource,
|
|
103
114
|
}
|
|
104
115
|
return format_to_item_type.get(format, ItemType.resource)
|
|
105
116
|
|
|
@@ -267,7 +278,7 @@ class Item:
|
|
|
267
278
|
|
|
268
279
|
# Optional execution context. Useful for letting functions that take only an Item
|
|
269
280
|
# arg get access to context.
|
|
270
|
-
context:
|
|
281
|
+
context: ActionContext | None = field(default=None, metadata={"exclude": True})
|
|
271
282
|
|
|
272
283
|
# These fields we don't want in YAML frontmatter.
|
|
273
284
|
# We don't include store_path as it's redundant with the filename.
|
|
@@ -369,9 +380,9 @@ class Item:
|
|
|
369
380
|
item_type: ItemType | None = None,
|
|
370
381
|
*,
|
|
371
382
|
title: str | None = None,
|
|
372
|
-
original_filename: str | None = None,
|
|
373
383
|
url: Url | None = None,
|
|
374
384
|
mime_type: MimeType | None = None,
|
|
385
|
+
preserve_filename: bool = True,
|
|
375
386
|
) -> Item:
|
|
376
387
|
"""
|
|
377
388
|
Create a resource Item for a file with a format inferred from the file extension
|
|
@@ -400,9 +411,10 @@ class Item:
|
|
|
400
411
|
if not file_ext:
|
|
401
412
|
file_ext = format_info.suggested_file_ext
|
|
402
413
|
|
|
414
|
+
original_filename = Path(path).name if preserve_filename else None
|
|
403
415
|
item = cls(
|
|
404
416
|
type=item_type,
|
|
405
|
-
title=title,
|
|
417
|
+
title=single_line(title) if title else None, # Avoid multiline titles.
|
|
406
418
|
file_ext=file_ext,
|
|
407
419
|
format=format,
|
|
408
420
|
external_path=str(path),
|
|
@@ -428,7 +440,7 @@ class Item:
|
|
|
428
440
|
return cls(
|
|
429
441
|
type=ItemType.resource,
|
|
430
442
|
format=Format.url,
|
|
431
|
-
title=media_metadata.title,
|
|
443
|
+
title=single_line(media_metadata.title), # Avoid multiline titles.
|
|
432
444
|
url=media_metadata.url,
|
|
433
445
|
description=media_metadata.description,
|
|
434
446
|
thumbnail_url=media_metadata.thumbnail_url,
|
|
@@ -453,7 +465,7 @@ class Item:
|
|
|
453
465
|
if self.type.expects_body and self.format.has_body and not self.body:
|
|
454
466
|
raise ValueError(f"Item type `{self.type.value}` is text but has no body: {self}")
|
|
455
467
|
|
|
456
|
-
def absolute_path(self, ws: Workspace | None = None) -> Path:
|
|
468
|
+
def absolute_path(self, ws: Path | Workspace | None = None) -> Path:
|
|
457
469
|
"""
|
|
458
470
|
Get the absolute path to the item. Throws `ValueError` if the item has no
|
|
459
471
|
store path. If no workspace is provided, uses the current workspace.
|
|
@@ -462,8 +474,11 @@ class Item:
|
|
|
462
474
|
|
|
463
475
|
if not self.store_path:
|
|
464
476
|
raise ValueError("Item has no store path")
|
|
465
|
-
|
|
477
|
+
elif isinstance(ws, Path):
|
|
478
|
+
return ws / self.store_path
|
|
479
|
+
elif not ws:
|
|
466
480
|
ws = current_ws()
|
|
481
|
+
|
|
467
482
|
return ws.base_dir / self.store_path
|
|
468
483
|
|
|
469
484
|
@property
|
|
@@ -485,7 +500,7 @@ class Item:
|
|
|
485
500
|
raise ValueError("Cannot get doc id for an item that has not been saved")
|
|
486
501
|
return str(self.store_path)
|
|
487
502
|
|
|
488
|
-
def metadata(self, datetime_as_str: bool = False) -> dict[str, Any]:
|
|
503
|
+
def metadata(self, *, datetime_as_str: bool = False) -> dict[str, Any]:
|
|
489
504
|
"""
|
|
490
505
|
Metadata is all relevant non-None fields in easy-to-serialize form.
|
|
491
506
|
Optional fields are omitted unless they are set.
|
|
@@ -529,6 +544,15 @@ class Item:
|
|
|
529
544
|
|
|
530
545
|
return item_dict
|
|
531
546
|
|
|
547
|
+
def sidematter(self, ws: Path | Workspace | None = None) -> ResolvedSidematter:
|
|
548
|
+
"""
|
|
549
|
+
Get the sidematter for this item, if present, by looking at the files
|
|
550
|
+
in the specified workspace (or the current workspace if not specified).
|
|
551
|
+
"""
|
|
552
|
+
from sidematter_format import Sidematter
|
|
553
|
+
|
|
554
|
+
return Sidematter(self.absolute_path(ws)).resolve()
|
|
555
|
+
|
|
532
556
|
def filename_stem(self) -> str | None:
|
|
533
557
|
"""
|
|
534
558
|
If the item has an existing or previous filename, return its stem,
|
|
@@ -545,16 +569,25 @@ class Item:
|
|
|
545
569
|
path_name = None
|
|
546
570
|
return path_name
|
|
547
571
|
|
|
548
|
-
def slug_name(
|
|
572
|
+
def slug_name(
|
|
573
|
+
self,
|
|
574
|
+
max_len: int = SLUG_MAX_LEN,
|
|
575
|
+
prefer_title: bool = False,
|
|
576
|
+
add_ops_suffix: bool = True,
|
|
577
|
+
) -> str:
|
|
549
578
|
"""
|
|
550
579
|
Get a readable slugified name for this item, either from a previous filename
|
|
551
|
-
or from slugifying the title or content.
|
|
580
|
+
or from slugifying the title or content. Adds the last operation suffix if requested
|
|
581
|
+
and available in item history.
|
|
552
582
|
"""
|
|
553
|
-
|
|
554
|
-
if
|
|
555
|
-
|
|
583
|
+
base_title = self.filename_stem()
|
|
584
|
+
if prefer_title or not base_title:
|
|
585
|
+
base_title = self.pick_title(max_len=max_len, add_ops_suffix=add_ops_suffix)
|
|
556
586
|
else:
|
|
557
|
-
|
|
587
|
+
base_title = self.pick_title(
|
|
588
|
+
base_title=base_title, max_len=max_len, add_ops_suffix=add_ops_suffix
|
|
589
|
+
)
|
|
590
|
+
return slugify_snake(base_title)
|
|
558
591
|
|
|
559
592
|
def default_filename(self) -> str:
|
|
560
593
|
"""
|
|
@@ -578,6 +611,7 @@ class Item:
|
|
|
578
611
|
|
|
579
612
|
def pick_title(
|
|
580
613
|
self,
|
|
614
|
+
base_title: str | None = None,
|
|
581
615
|
*,
|
|
582
616
|
max_len: int = 100,
|
|
583
617
|
add_ops_suffix: bool = False,
|
|
@@ -590,35 +624,31 @@ class Item:
|
|
|
590
624
|
"""
|
|
591
625
|
# First special case: if we are pulling the title from the body header, check
|
|
592
626
|
# that.
|
|
593
|
-
if not
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
627
|
+
if not base_title:
|
|
628
|
+
if not base_title and pull_body_heading:
|
|
629
|
+
heading = self.body_heading()
|
|
630
|
+
base_title = heading
|
|
631
|
+
|
|
632
|
+
# Next special case: URLs with no title use the url itself.
|
|
633
|
+
if not self.title and self.url:
|
|
634
|
+
return abbrev_str(self.url, max_len)
|
|
635
|
+
|
|
636
|
+
filename_stem = self.filename_stem()
|
|
637
|
+
|
|
638
|
+
# Use the title or the path if possible, falling back to description or even body text.
|
|
639
|
+
base_title = (
|
|
640
|
+
self.title
|
|
641
|
+
or filename_stem
|
|
642
|
+
or self.description
|
|
643
|
+
or (not self.is_binary and self.abbrev_body(max_len))
|
|
644
|
+
or UNTITLED
|
|
645
|
+
)
|
|
612
646
|
|
|
613
|
-
suffix = ""
|
|
614
647
|
# For docs, etc but not for concepts/resources/exports, add a parenthical note
|
|
615
648
|
# indicating the last operation, if there was one. This makes filename slugs
|
|
616
649
|
# more readable.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
ItemType.resource,
|
|
620
|
-
ItemType.export,
|
|
621
|
-
]:
|
|
650
|
+
suffix = ""
|
|
651
|
+
if add_ops_suffix and self.type.allows_op_suffix:
|
|
622
652
|
last_op = self.history and self.history[-1].action_name
|
|
623
653
|
if last_op:
|
|
624
654
|
step_num = len(self.history) + 1 if self.history else 1
|
|
@@ -626,7 +656,7 @@ class Item:
|
|
|
626
656
|
|
|
627
657
|
shorter_len = min(max_len, max(max_len - len(suffix), 20))
|
|
628
658
|
clean_text = sanitize_title(
|
|
629
|
-
abbrev_phrase_in_middle(html_to_plaintext(
|
|
659
|
+
abbrev_phrase_in_middle(html_to_plaintext(base_title), shorter_len)
|
|
630
660
|
)
|
|
631
661
|
|
|
632
662
|
final_text = clean_text
|
|
@@ -670,8 +700,6 @@ class Item:
|
|
|
670
700
|
"""
|
|
671
701
|
If it is a data Item, return the parsed YAML.
|
|
672
702
|
"""
|
|
673
|
-
if not self.type == ItemType.data:
|
|
674
|
-
raise FileFormatError(f"Item is not a data item: {self}")
|
|
675
703
|
if not self.body:
|
|
676
704
|
raise FileFormatError(f"Data item has no body: {self}")
|
|
677
705
|
if self.format != Format.yaml:
|
|
@@ -795,15 +823,22 @@ class Item:
|
|
|
795
823
|
merged_fields = self._copy_and_update(other, update_timestamp=False)
|
|
796
824
|
return Item(**merged_fields)
|
|
797
825
|
|
|
798
|
-
def derived_copy(
|
|
826
|
+
def derived_copy(
|
|
827
|
+
self,
|
|
828
|
+
action_context: ActionContext | None = None,
|
|
829
|
+
output_num: int = 0,
|
|
830
|
+
**updates: Unpack[ItemUpdateOptions],
|
|
831
|
+
) -> Item:
|
|
799
832
|
"""
|
|
800
833
|
Copy item with the given field updates. Resets `store_path` and `source` to None
|
|
801
834
|
since those should be set explicitly later. Preserves other fields, including
|
|
802
|
-
the body.
|
|
835
|
+
the type and the body.
|
|
803
836
|
|
|
804
837
|
Same as `new_copy_with` but also updates the `derived_from` relation. If we also
|
|
805
838
|
have an action context, then use the `title_template` to derive a new title.
|
|
806
839
|
"""
|
|
840
|
+
|
|
841
|
+
# Get derived_from relation if possible.
|
|
807
842
|
if not self.store_path:
|
|
808
843
|
if self.relations.derived_from:
|
|
809
844
|
log.message(
|
|
@@ -830,30 +865,40 @@ class Item:
|
|
|
830
865
|
assert updates["format"] is not None
|
|
831
866
|
updates["file_ext"] = updates["format"].file_ext
|
|
832
867
|
|
|
833
|
-
# External resource paths
|
|
834
|
-
|
|
835
|
-
new_type = updates.get("type") or self.type
|
|
836
|
-
if "external_path" not in updates and new_type != ItemType.resource:
|
|
868
|
+
# External resource paths should not be preserved.
|
|
869
|
+
if "external_path" not in updates:
|
|
837
870
|
updates["external_path"] = None
|
|
838
871
|
|
|
839
872
|
new_item = self.new_copy_with(update_timestamp=True, **updates)
|
|
840
873
|
if derived_from:
|
|
841
874
|
new_item.update_relations(derived_from=derived_from)
|
|
842
875
|
|
|
876
|
+
action_context = action_context or self.context
|
|
877
|
+
|
|
878
|
+
# Record the history.
|
|
879
|
+
if action_context:
|
|
880
|
+
self.source = Source(
|
|
881
|
+
operation=action_context.operation,
|
|
882
|
+
output_num=output_num,
|
|
883
|
+
cacheable=action_context.action.cacheable,
|
|
884
|
+
)
|
|
885
|
+
self.add_to_history(self.source.operation.summary())
|
|
886
|
+
action = action_context.action
|
|
887
|
+
else:
|
|
888
|
+
action = None
|
|
889
|
+
|
|
843
890
|
# Fall back to action title template if we have it and title wasn't explicitly set.
|
|
844
891
|
if "title" not in updates:
|
|
845
892
|
prev_title = self.title or (Path(self.store_path).stem if self.store_path else UNTITLED)
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
new_item.title = action.
|
|
849
|
-
title=prev_title, action_name=action.name
|
|
850
|
-
)
|
|
893
|
+
|
|
894
|
+
if action:
|
|
895
|
+
new_item.title = action.format_title(prev_title)
|
|
851
896
|
else:
|
|
852
|
-
log.
|
|
897
|
+
log.info(
|
|
853
898
|
"Deriving an item without action context so keeping previous title: %s",
|
|
854
899
|
self,
|
|
855
900
|
)
|
|
856
|
-
new_item.title =
|
|
901
|
+
new_item.title = prev_title
|
|
857
902
|
|
|
858
903
|
return new_item
|
|
859
904
|
|
|
@@ -906,6 +951,17 @@ class Item:
|
|
|
906
951
|
if not self.history or self.history[-1] != operation_summary:
|
|
907
952
|
self.history.append(operation_summary)
|
|
908
953
|
|
|
954
|
+
def mark_as_saved(self, external_path: Path) -> None:
|
|
955
|
+
"""
|
|
956
|
+
Mark the item as saved at an external or internal path. If this item is saved to a
|
|
957
|
+
workspace and the path is inside the workspace, the save will be short-circuited.
|
|
958
|
+
If it's outside the workspace, the item will be copied to the workspace.
|
|
959
|
+
Having this method makes it quick to catch bugs where the file is missing.
|
|
960
|
+
"""
|
|
961
|
+
if not external_path.exists():
|
|
962
|
+
raise FileNotFoundError(f"Provided path not found: {fmt_loc(external_path)}")
|
|
963
|
+
self.external_path = str(external_path)
|
|
964
|
+
|
|
909
965
|
def fmt_loc(self) -> str:
|
|
910
966
|
"""
|
|
911
967
|
Formatted store path, external path, URL, or title. Use for logging etc.
|
|
@@ -949,6 +1005,7 @@ class Item:
|
|
|
949
1005
|
key_filter={
|
|
950
1006
|
"store_path": 0,
|
|
951
1007
|
"external_path": 64,
|
|
1008
|
+
"original_filename": 64,
|
|
952
1009
|
"type": 64,
|
|
953
1010
|
"format": 64,
|
|
954
1011
|
"state": 64,
|
kash/model/params_model.py
CHANGED
|
@@ -206,10 +206,10 @@ A list of parameter declarations, possibly with default values.
|
|
|
206
206
|
|
|
207
207
|
# These are the default models for typical use cases.
|
|
208
208
|
# The user may override them with parameters.
|
|
209
|
-
DEFAULT_CAREFUL_LLM = LLM.
|
|
210
|
-
DEFAULT_STRUCTURED_LLM = LLM.
|
|
211
|
-
DEFAULT_STANDARD_LLM = LLM.
|
|
212
|
-
DEFAULT_FAST_LLM = LLM.
|
|
209
|
+
DEFAULT_CAREFUL_LLM = LLM.gpt_5
|
|
210
|
+
DEFAULT_STRUCTURED_LLM = LLM.gpt_5
|
|
211
|
+
DEFAULT_STANDARD_LLM = LLM.gpt_5
|
|
212
|
+
DEFAULT_FAST_LLM = LLM.gpt_5_mini
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
# Parameters set globally such as in the workspace.
|
|
@@ -10,7 +10,6 @@ from enum import Enum, auto
|
|
|
10
10
|
import rich
|
|
11
11
|
import rich.style
|
|
12
12
|
from flowmark import Wrap, fill_text
|
|
13
|
-
from flowmark.text_filling import DEFAULT_INDENT
|
|
14
13
|
from rich.console import Group, OverflowMethod, RenderableType
|
|
15
14
|
from rich.rule import Rule
|
|
16
15
|
from rich.style import Style
|
|
@@ -50,7 +49,7 @@ def print_style(pad_style: PadStyle):
|
|
|
50
49
|
Context manager for print styles.
|
|
51
50
|
"""
|
|
52
51
|
if pad_style == PadStyle.INDENT:
|
|
53
|
-
token = print_context_var.set(
|
|
52
|
+
token = print_context_var.set(" ")
|
|
54
53
|
try:
|
|
55
54
|
yield
|
|
56
55
|
finally:
|
|
@@ -93,7 +93,6 @@ content: |
|
|
|
93
93
|
|
|
94
94
|
from __future__ import annotations
|
|
95
95
|
|
|
96
|
-
import json
|
|
97
96
|
from dataclasses import field
|
|
98
97
|
from enum import Enum
|
|
99
98
|
from io import StringIO
|
|
@@ -104,6 +103,7 @@ from typing import Any
|
|
|
104
103
|
from frontmatter_format import from_yaml_string, new_yaml, to_yaml_string
|
|
105
104
|
from prettyfmt import abbrev_obj, custom_key_sort, fmt_size_human
|
|
106
105
|
from pydantic.dataclasses import dataclass
|
|
106
|
+
from sidematter_format import to_json_string
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
class ChatRole(str, Enum):
|
|
@@ -161,9 +161,12 @@ class ChatMessage:
|
|
|
161
161
|
Convert to a format that can be used as a standard chat completion, with
|
|
162
162
|
the content field holding JSON-serialized data if it is structured.
|
|
163
163
|
"""
|
|
164
|
+
|
|
164
165
|
return {
|
|
165
166
|
"role": self.role.value,
|
|
166
|
-
"content":
|
|
167
|
+
"content": to_json_string(self.content)
|
|
168
|
+
if isinstance(self.content, dict)
|
|
169
|
+
else self.content,
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
@classmethod
|
|
@@ -174,7 +177,7 @@ class ChatMessage:
|
|
|
174
177
|
return to_yaml_string(self.as_dict(), key_sort=_custom_key_sort)
|
|
175
178
|
|
|
176
179
|
def to_json(self) -> str:
|
|
177
|
-
return
|
|
180
|
+
return to_json_string(self.as_dict())
|
|
178
181
|
|
|
179
182
|
def as_str(self) -> str:
|
|
180
183
|
return self.to_yaml()
|
|
@@ -222,7 +225,7 @@ class ChatHistory:
|
|
|
222
225
|
return stream.getvalue()
|
|
223
226
|
|
|
224
227
|
def to_json(self) -> str:
|
|
225
|
-
return
|
|
228
|
+
return to_json_string([message.as_dict() for message in self.messages], indent=None)
|
|
226
229
|
|
|
227
230
|
def size_summary(self) -> str:
|
|
228
231
|
role_counts = {}
|
|
@@ -16,7 +16,7 @@ def is_fullpage_html(content: str) -> bool:
|
|
|
16
16
|
A full HTML document that is a full page (headers, footers, etc.) and
|
|
17
17
|
so probably best rendered in a browser.
|
|
18
18
|
"""
|
|
19
|
-
return bool(re.search(r"<!DOCTYPE html>|<html
|
|
19
|
+
return bool(re.search(r"<!DOCTYPE html>|<html.*?>|<body>|<head>", content, re.IGNORECASE))
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
_yaml_header_pattern = re.compile(r"^---\n\w+:", re.MULTILINE)
|
|
@@ -35,7 +35,9 @@ def is_html(content: str) -> bool:
|
|
|
35
35
|
"""
|
|
36
36
|
return bool(
|
|
37
37
|
re.search(
|
|
38
|
-
r"<!DOCTYPE html>|<html
|
|
38
|
+
r"<!DOCTYPE html>|<html.*?>|<body>|<head>|<div>|<p>|<img |<a href",
|
|
39
|
+
content,
|
|
40
|
+
re.IGNORECASE,
|
|
39
41
|
)
|
|
40
42
|
)
|
|
41
43
|
|
|
@@ -72,6 +72,9 @@ class Format(Enum):
|
|
|
72
72
|
mp3 = "mp3"
|
|
73
73
|
m4a = "m4a"
|
|
74
74
|
mp4 = "mp4"
|
|
75
|
+
|
|
76
|
+
# Binary formats.
|
|
77
|
+
zip = "zip"
|
|
75
78
|
binary = "binary"
|
|
76
79
|
"""Catch-all format for binary files that are unrecognized."""
|
|
77
80
|
|
|
@@ -167,6 +170,10 @@ class Format(Enum):
|
|
|
167
170
|
def is_data(self) -> bool:
|
|
168
171
|
return self in [self.csv, self.xlsx, self.npz]
|
|
169
172
|
|
|
173
|
+
@property
|
|
174
|
+
def is_zip(self) -> bool:
|
|
175
|
+
return self in [self.zip]
|
|
176
|
+
|
|
170
177
|
@property
|
|
171
178
|
def is_binary(self) -> bool:
|
|
172
179
|
return self.has_body and not self.is_text
|
|
@@ -257,6 +264,7 @@ class Format(Enum):
|
|
|
257
264
|
FileExt.m4a.value: Format.m4a,
|
|
258
265
|
FileExt.mp4.value: Format.mp4,
|
|
259
266
|
FileExt.epub.value: Format.epub,
|
|
267
|
+
FileExt.zip.value: Format.zip,
|
|
260
268
|
}
|
|
261
269
|
return ext_to_format.get(file_ext.value, None)
|
|
262
270
|
|
|
@@ -292,6 +300,7 @@ class Format(Enum):
|
|
|
292
300
|
Format.mp3: FileExt.mp3,
|
|
293
301
|
Format.m4a: FileExt.m4a,
|
|
294
302
|
Format.mp4: FileExt.mp4,
|
|
303
|
+
Format.zip: FileExt.zip,
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
return format_to_file_ext.get(self, None)
|
|
@@ -329,6 +338,9 @@ class Format(Enum):
|
|
|
329
338
|
"audio/mp3": Format.mp3,
|
|
330
339
|
"audio/mp4": Format.m4a,
|
|
331
340
|
"video/mp4": Format.mp4,
|
|
341
|
+
"application/zip": Format.zip,
|
|
342
|
+
"application/x-zip": Format.zip,
|
|
343
|
+
"application/x-zip-compressed": Format.zip,
|
|
332
344
|
"application/octet-stream": Format.binary,
|
|
333
345
|
}
|
|
334
346
|
|
|
@@ -75,7 +75,7 @@ def normalize_text_file(
|
|
|
75
75
|
|
|
76
76
|
def test_osc8_link():
|
|
77
77
|
from clideps.terminal.osc_utils import osc8_link
|
|
78
|
-
from flowmark
|
|
78
|
+
from flowmark import wrap_paragraph
|
|
79
79
|
|
|
80
80
|
link = osc8_link("https://example.com/" + "x" * 50, "Example")
|
|
81
81
|
assert ansi_cell_len(link) == 7
|