kash-shell 0.3.8__py3-none-any.whl → 0.3.10__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 (154) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/markdownify.py +5 -2
  3. kash/actions/core/readability.py +5 -2
  4. kash/actions/core/render_as_html.py +18 -0
  5. kash/actions/core/webpage_config.py +12 -4
  6. kash/commands/__init__.py +8 -20
  7. kash/commands/base/basic_file_commands.py +15 -0
  8. kash/commands/base/debug_commands.py +15 -2
  9. kash/commands/base/general_commands.py +27 -18
  10. kash/commands/base/logs_commands.py +1 -4
  11. kash/commands/base/model_commands.py +8 -8
  12. kash/commands/base/search_command.py +3 -2
  13. kash/commands/base/show_command.py +5 -3
  14. kash/commands/extras/parse_uv_lock.py +186 -0
  15. kash/commands/help/doc_commands.py +2 -31
  16. kash/commands/help/welcome.py +33 -0
  17. kash/commands/workspace/selection_commands.py +11 -6
  18. kash/commands/workspace/workspace_commands.py +19 -16
  19. kash/config/colors.py +2 -0
  20. kash/config/env_settings.py +72 -0
  21. kash/config/init.py +2 -2
  22. kash/config/logger.py +61 -59
  23. kash/config/logger_basic.py +12 -5
  24. kash/config/server_config.py +6 -6
  25. kash/config/settings.py +117 -67
  26. kash/config/setup.py +35 -9
  27. kash/config/suppress_warnings.py +30 -12
  28. kash/config/text_styles.py +3 -13
  29. kash/docs/load_api_docs.py +2 -1
  30. kash/docs/markdown/topics/a2_installation.md +7 -3
  31. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  32. kash/docs/markdown/warning.md +3 -8
  33. kash/docs/markdown/welcome.md +4 -0
  34. kash/docs_base/load_recipe_snippets.py +1 -1
  35. kash/docs_base/recipes/{general_system_commands.ksh → general_system_commands.sh} +1 -1
  36. kash/{concepts → embeddings}/cosine.py +2 -1
  37. kash/embeddings/text_similarity.py +57 -0
  38. kash/exec/__init__.py +20 -3
  39. kash/exec/action_decorators.py +18 -4
  40. kash/exec/action_exec.py +41 -23
  41. kash/exec/action_registry.py +13 -48
  42. kash/exec/command_registry.py +2 -1
  43. kash/exec/fetch_url_metadata.py +4 -6
  44. kash/exec/importing.py +56 -0
  45. kash/exec/llm_transforms.py +6 -6
  46. kash/exec/precondition_registry.py +2 -1
  47. kash/exec/preconditions.py +16 -1
  48. kash/exec/shell_callable_action.py +33 -19
  49. kash/file_storage/file_store.py +23 -14
  50. kash/file_storage/item_file_format.py +13 -3
  51. kash/file_storage/metadata_dirs.py +11 -2
  52. kash/help/assistant.py +2 -2
  53. kash/help/assistant_instructions.py +2 -1
  54. kash/help/help_embeddings.py +2 -2
  55. kash/help/help_printing.py +14 -10
  56. kash/help/tldr_help.py +5 -3
  57. kash/llm_utils/clean_headings.py +1 -1
  58. kash/llm_utils/llm_api_keys.py +4 -4
  59. kash/llm_utils/llm_completion.py +2 -2
  60. kash/llm_utils/llm_features.py +68 -0
  61. kash/llm_utils/llm_messages.py +1 -2
  62. kash/llm_utils/llm_names.py +1 -1
  63. kash/llm_utils/llms.py +17 -12
  64. kash/local_server/__init__.py +5 -2
  65. kash/local_server/local_server.py +56 -46
  66. kash/local_server/local_server_commands.py +15 -15
  67. kash/local_server/local_server_routes.py +2 -2
  68. kash/local_server/local_url_formatters.py +1 -1
  69. kash/mcp/__init__.py +5 -2
  70. kash/mcp/mcp_cli.py +54 -17
  71. kash/mcp/mcp_server_commands.py +5 -6
  72. kash/mcp/mcp_server_routes.py +14 -11
  73. kash/mcp/mcp_server_sse.py +61 -34
  74. kash/mcp/mcp_server_stdio.py +0 -8
  75. kash/media_base/audio_processing.py +81 -7
  76. kash/media_base/media_cache.py +18 -18
  77. kash/media_base/media_services.py +1 -1
  78. kash/media_base/media_tools.py +6 -6
  79. kash/media_base/services/local_file_media.py +2 -2
  80. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -109
  81. kash/media_base/transcription_format.py +73 -0
  82. kash/media_base/transcription_whisper.py +38 -0
  83. kash/model/__init__.py +73 -5
  84. kash/model/actions_model.py +38 -4
  85. kash/model/concept_model.py +30 -0
  86. kash/model/items_model.py +56 -13
  87. kash/model/params_model.py +24 -0
  88. kash/shell/completions/completion_scoring.py +37 -5
  89. kash/shell/output/kerm_codes.py +1 -2
  90. kash/shell/output/shell_formatting.py +14 -4
  91. kash/shell/shell_main.py +2 -2
  92. kash/shell/utils/exception_printing.py +6 -0
  93. kash/shell/utils/native_utils.py +26 -20
  94. kash/text_handling/custom_sliding_transforms.py +12 -4
  95. kash/text_handling/doc_normalization.py +6 -2
  96. kash/text_handling/markdown_render.py +117 -0
  97. kash/text_handling/markdown_utils.py +204 -0
  98. kash/utils/common/import_utils.py +12 -3
  99. kash/utils/common/type_utils.py +0 -29
  100. kash/utils/common/url.py +80 -28
  101. kash/utils/errors.py +6 -0
  102. kash/utils/file_utils/{dir_size.py → dir_info.py} +25 -4
  103. kash/utils/file_utils/file_ext.py +2 -3
  104. kash/utils/file_utils/file_formats.py +28 -2
  105. kash/utils/file_utils/file_formats_model.py +50 -19
  106. kash/utils/file_utils/filename_parsing.py +10 -4
  107. kash/web_content/dir_store.py +1 -2
  108. kash/web_content/file_cache_utils.py +37 -10
  109. kash/web_content/file_processing.py +68 -0
  110. kash/web_content/local_file_cache.py +12 -9
  111. kash/web_content/web_extract.py +8 -3
  112. kash/web_content/web_fetch.py +12 -4
  113. kash/web_gen/tabbed_webpage.py +5 -2
  114. kash/web_gen/templates/base_styles.css.jinja +120 -14
  115. kash/web_gen/templates/base_webpage.html.jinja +60 -13
  116. kash/web_gen/templates/content_styles.css.jinja +4 -2
  117. kash/web_gen/templates/item_view.html.jinja +2 -2
  118. kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
  119. kash/workspaces/__init__.py +15 -2
  120. kash/workspaces/selections.py +18 -3
  121. kash/workspaces/source_items.py +4 -2
  122. kash/workspaces/workspace_output.py +11 -4
  123. kash/workspaces/workspaces.py +5 -11
  124. kash/xonsh_custom/command_nl_utils.py +40 -19
  125. kash/xonsh_custom/custom_shell.py +44 -12
  126. kash/xonsh_custom/customize_prompt.py +39 -21
  127. kash/xonsh_custom/load_into_xonsh.py +26 -27
  128. kash/xonsh_custom/shell_load_commands.py +2 -2
  129. kash/xonsh_custom/xonsh_completers.py +2 -249
  130. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  131. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  132. kash/xontrib/kash_extension.py +5 -6
  133. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/METADATA +26 -12
  134. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/RECORD +140 -140
  135. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +1 -1
  136. kash/concepts/concept_formats.py +0 -23
  137. kash/concepts/text_similarity.py +0 -112
  138. kash/shell/clideps/api_keys.py +0 -99
  139. kash/shell/clideps/dotenv_setup.py +0 -114
  140. kash/shell/clideps/dotenv_utils.py +0 -89
  141. kash/shell/clideps/pkg_deps.py +0 -232
  142. kash/shell/clideps/platforms.py +0 -11
  143. kash/shell/clideps/terminal_features.py +0 -56
  144. kash/shell/utils/osc_utils.py +0 -95
  145. kash/shell/utils/terminal_images.py +0 -133
  146. kash/text_handling/markdown_util.py +0 -167
  147. kash/utils/common/atomic_var.py +0 -158
  148. kash/utils/common/string_replace.py +0 -93
  149. kash/utils/common/string_template.py +0 -101
  150. /kash/docs_base/recipes/{python_dev_commands.ksh → python_dev_commands.sh} +0 -0
  151. /kash/docs_base/recipes/{tldr_standard_commands.ksh → tldr_standard_commands.sh} +0 -0
  152. /kash/{concepts → embeddings}/embeddings.py +0 -0
  153. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
  154. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/licenses/LICENSE +0 -0
@@ -22,7 +22,7 @@ _item_cache = MtimeCache[Item](max_size=2000, name="Item")
22
22
 
23
23
 
24
24
  @tally_calls()
25
- def write_item(item: Item, path: Path):
25
+ def write_item(item: Item, path: Path, normalize: bool = True):
26
26
  """
27
27
  Write a text item to a file with standard frontmatter format YAML.
28
28
  Also normalizes formatting of the body text.
@@ -36,7 +36,10 @@ def write_item(item: Item, path: Path):
36
36
  # Clear cache before writing.
37
37
  _item_cache.delete(path)
38
38
 
39
- body = normalize_formatting_ansi(item.body_text(), item.format)
39
+ if normalize:
40
+ body = normalize_formatting_ansi(item.body_text(), item.format)
41
+ else:
42
+ body = item.body_text()
40
43
 
41
44
  # Special case for YAML files to avoid a possible duplicate `---` divider in the body.
42
45
  if body and item.format == Format.yaml:
@@ -48,7 +51,14 @@ def write_item(item: Item, path: Path):
48
51
  format = Format(item.format)
49
52
  if format == Format.html:
50
53
  fm_style = FmStyle.html
51
- elif format in [Format.python, Format.kash_script, Format.diff, Format.csv, Format.log]:
54
+ elif format in [
55
+ Format.python,
56
+ Format.shellscript,
57
+ Format.xonsh,
58
+ Format.diff,
59
+ Format.csv,
60
+ Format.log,
61
+ ]:
52
62
  fm_style = FmStyle.hash
53
63
  elif format == Format.json:
54
64
  fm_style = FmStyle.slash
@@ -7,7 +7,12 @@ from pathlib import Path
7
7
  from pydantic.dataclasses import dataclass
8
8
 
9
9
  from kash.config.logger import get_logger
10
- from kash.config.settings import CONTENT_CACHE_NAME, DOT_DIR, MEDIA_CACHE_NAME, get_system_cache_dir
10
+ from kash.config.settings import (
11
+ CONTENT_CACHE_NAME,
12
+ DOT_DIR,
13
+ MEDIA_CACHE_NAME,
14
+ global_settings,
15
+ )
11
16
  from kash.file_storage.persisted_yaml import PersistedYaml
12
17
  from kash.model.paths_model import StorePath
13
18
  from kash.utils.common.format_utils import fmt_loc
@@ -62,7 +67,11 @@ class MetadataDirs:
62
67
  # in which case it is in the global cache path.
63
68
  @property
64
69
  def cache_dir(self) -> Path:
65
- return get_system_cache_dir() if self.is_global_ws else StorePath(f"{DOT_DIR}/cache")
70
+ return (
71
+ global_settings().system_cache_dir
72
+ if self.is_global_ws
73
+ else StorePath(f"{DOT_DIR}/cache")
74
+ )
66
75
 
67
76
  @property
68
77
  def media_cache_dir(self) -> Path:
kash/help/assistant.py CHANGED
@@ -110,7 +110,7 @@ def assist_current_state() -> Message:
110
110
  ws_info = f"Based on the current directory, the current workspace is: {ws_base_dir.name} at {fmt_loc(ws_base_dir)}"
111
111
  else:
112
112
  if is_global_ws:
113
- about_ws = "You are currently using the `global` workspace."
113
+ about_ws = "You are currently using the default global workspace."
114
114
  else:
115
115
  about_ws = "The current directory is not a workspace."
116
116
  ws_info = (
@@ -318,7 +318,7 @@ def shell_context_assistance(
318
318
  type=ItemType.script,
319
319
  title=f"Assistant Answer: {capitalize_cms(input)}",
320
320
  description=response_text,
321
- format=Format.kash_script,
321
+ format=Format.shellscript,
322
322
  body=script.script_str,
323
323
  )
324
324
  ws = current_ws()
@@ -1,10 +1,11 @@
1
1
  from functools import cache
2
2
  from textwrap import dedent
3
3
 
4
+ from strif import StringTemplate
5
+
4
6
  from kash.config.logger import get_logger
5
7
  from kash.docs.all_docs import all_docs
6
8
  from kash.docs.load_help_topics import load_help_src
7
- from kash.utils.common.string_template import StringTemplate
8
9
 
9
10
  log = get_logger(__name__)
10
11
 
@@ -3,9 +3,9 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
5
 
6
- from kash.concepts.embeddings import Embeddings
7
- from kash.concepts.text_similarity import rank_by_relatedness
8
6
  from kash.config.logger import get_logger
7
+ from kash.embeddings.embeddings import Embeddings
8
+ from kash.embeddings.text_similarity import rank_by_relatedness
9
9
  from kash.help.help_types import HelpDoc, HelpDocType
10
10
  from kash.web_content.local_file_cache import Loadable
11
11
 
@@ -1,5 +1,8 @@
1
+ from typing import Any
2
+
1
3
  from kash.config.logger import get_logger
2
4
  from kash.config.text_styles import STYLE_HINT
5
+ from kash.docs.all_docs import DocSelection
3
6
  from kash.exec.action_registry import look_up_action_class
4
7
  from kash.exec.command_registry import CommandFunction, look_up_command
5
8
  from kash.help.assistant import assist_preamble, assistance_unstructured
@@ -10,7 +13,7 @@ from kash.help.tldr_help import tldr_help
10
13
  from kash.llm_utils import LLM
11
14
  from kash.llm_utils.llm_messages import Message
12
15
  from kash.model.actions_model import Action
13
- from kash.model.params_model import COMMON_SHELL_PARAMS, RUNTIME_ACTION_PARAMS, Param
16
+ from kash.model.params_model import COMMON_SHELL_PARAMS, Param
14
17
  from kash.model.preconditions_model import Precondition
15
18
  from kash.shell.output.shell_formatting import format_name_and_description, format_name_and_value
16
19
  from kash.shell.output.shell_output import (
@@ -32,7 +35,7 @@ GENERAL_HELP = (
32
35
  def _print_command_help(
33
36
  name: str,
34
37
  description: str | None = None,
35
- param_info: list[Param] | None = None,
38
+ param_info: list[Param[Any]] | None = None,
36
39
  precondition: Precondition | None = None,
37
40
  verbose: bool = True,
38
41
  is_action: bool = False, # pyright: ignore[reportUnusedParameter]
@@ -91,18 +94,19 @@ def print_command_function_help(command: CommandFunction, verbose: bool = True):
91
94
  )
92
95
 
93
96
 
94
- def print_action_help(action: Action, verbose: bool = True):
95
- params = (
96
- list(action.params)
97
- + list(RUNTIME_ACTION_PARAMS.values())
98
- + list(COMMON_SHELL_PARAMS.values())
99
- )
97
+ def print_action_help(
98
+ action: Action,
99
+ verbose: bool = True,
100
+ include_options: bool = True,
101
+ include_precondition: bool = True,
102
+ ):
103
+ params = action.shell_params if include_options else None
100
104
 
101
105
  _print_command_help(
102
106
  action.name,
103
107
  action.description,
104
108
  param_info=params,
105
- precondition=action.precondition,
109
+ precondition=action.precondition if include_precondition else None,
106
110
  verbose=verbose,
107
111
  is_action=True,
108
112
  extra_note="(kash action)",
@@ -168,7 +172,7 @@ def print_explain_command(text: str, assistant_model: LLM | None = None):
168
172
  # Give the LLM full context on kash APIs.
169
173
  # But we do this here lazily to prevent circular dependencies.
170
174
  system_message = Message(
171
- assist_preamble(is_structured=False, skip_api_docs=False, base_actions_only=False)
175
+ assist_preamble(is_structured=False, doc_selection=DocSelection.full)
172
176
  )
173
177
  chat_history.extend(
174
178
  [
kash/help/tldr_help.py CHANGED
@@ -21,7 +21,7 @@ from tldr import (
21
21
  )
22
22
 
23
23
  from kash.config.logger import get_logger
24
- from kash.docs_base.load_recipe_snippets import RECIPES_DIR
24
+ from kash.docs_base.load_recipe_snippets import RECIPE_EXT, RECIPES_DIR
25
25
  from kash.exec_model.commands_model import CommentedCommand
26
26
  from kash.exec_model.script_model import BareComment
27
27
  from kash.help.help_types import CommandInfo, CommandType
@@ -289,11 +289,13 @@ def dump_all_tldr_snippets(commands: list[str] = RECOMMENDED_TLDR_COMMANDS) -> N
289
289
  log.warning("Including command not in a local path: %s", command)
290
290
  continue
291
291
 
292
- with atomic_output_file(RECIPES_DIR / "tldr_standard_commands.ksh") as tmp:
292
+ with atomic_output_file(RECIPES_DIR / ("tldr_standard_commands" + RECIPE_EXT)) as tmp:
293
293
  with open(tmp, "w") as f:
294
294
  print("# -- Generated by dump_tldr_snippets --", file=f)
295
295
  _write_tldr_snippets(commands, f)
296
296
 
297
297
  log.message(
298
- "Dumped %s TLDR snippets: %s", len(commands), RECIPES_DIR / "tldr_standard_commands.ksh"
298
+ "Dumped %s TLDR snippets: %s",
299
+ len(commands),
300
+ RECIPES_DIR / ("tldr_standard_commands" + RECIPE_EXT),
299
301
  )
@@ -1,6 +1,6 @@
1
1
  from kash.llm_utils import Message, MessageTemplate, llm_template_completion
2
2
  from kash.llm_utils.llms import LLM
3
- from kash.text_handling.markdown_util import as_bullet_points
3
+ from kash.text_handling.markdown_utils import as_bullet_points
4
4
 
5
5
  # TODO: Enforce that the edits below doesn't contain anything extraneous.
6
6
 
@@ -1,15 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import litellm
4
+ from clideps.env_vars.dotenv_utils import env_var_is_set
5
+ from clideps.env_vars.env_names import EnvName
4
6
  from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
5
7
 
6
8
  from kash.llm_utils.llm_names import LLMName
7
9
  from kash.llm_utils.llms import LLM
8
- from kash.shell.clideps.api_keys import ApiEnvKey
9
- from kash.shell.clideps.dotenv_utils import env_var_is_set
10
10
 
11
11
 
12
- def api_for_model(model: LLMName) -> ApiEnvKey | None:
12
+ def api_for_model(model: LLMName) -> EnvName | None:
13
13
  """
14
14
  Get the API key name for a model or None if not found.
15
15
  """
@@ -18,7 +18,7 @@ def api_for_model(model: LLMName) -> ApiEnvKey | None:
18
18
  except litellm.exceptions.BadRequestError:
19
19
  return None
20
20
 
21
- return ApiEnvKey.for_provider(custom_llm_provider)
21
+ return EnvName.api_env_name(custom_llm_provider)
22
22
 
23
23
 
24
24
  def have_key_for_model(model: LLMName) -> bool:
@@ -6,9 +6,9 @@ from flowmark import Wrap, fill_text
6
6
  from funlog import format_duration, log_calls
7
7
  from litellm.types.utils import Choices, ModelResponse
8
8
  from litellm.types.utils import Message as LiteLLMMessage
9
+ from prettyfmt import slugify_snake
9
10
  from pydantic import BaseModel
10
11
  from pydantic.dataclasses import dataclass
11
- from slugify import slugify
12
12
 
13
13
  from kash.config.logger import get_logger
14
14
  from kash.config.text_styles import EMOJI_TIMING
@@ -113,7 +113,7 @@ def llm_completion(
113
113
  chat_history.messages.append(
114
114
  ChatMessage(role=ChatRole.assistant, content=content, metadata=metadata)
115
115
  )
116
- model_slug = slugify(model.litellm_name, separator="_")
116
+ model_slug = slugify_snake(model.litellm_name)
117
117
  log.save_object(
118
118
  "LLM response",
119
119
  f"llm.{model_slug}",
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal, TypeAlias
5
+
6
+ from prettyfmt import custom_key_sort
7
+
8
+ from kash.llm_utils.llm_names import LLMName
9
+ from kash.llm_utils.llms import LLM
10
+
11
+ Speed: TypeAlias = Literal["fast", "medium", "slow"]
12
+
13
+ ContextSize: TypeAlias = Literal["small", "medium", "large"]
14
+
15
+ ModelSize: TypeAlias = Literal["small", "medium", "large"]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class LLMFeatures:
20
+ speed: Speed | None = None
21
+ context_size: ContextSize | None = None
22
+ model_size: ModelSize | None = None
23
+ structured_output: bool | None = None
24
+ thinking: bool = False
25
+
26
+ def satisfies(self, features: LLMFeatures) -> bool:
27
+ return all(
28
+ getattr(self, attr) == getattr(features, attr)
29
+ for attr in features.__dataclass_fields__
30
+ if getattr(self, attr) is not None
31
+ )
32
+
33
+
34
+ def pick_llm(desired_features: LLMFeatures) -> LLMName:
35
+ """
36
+ Pick the preferred model that satisfies the desired features.
37
+ """
38
+ satisfied_models: list[LLMName] = [
39
+ llm for llm, features in FEATURES.items() if features.satisfies(desired_features)
40
+ ]
41
+ satisfied_models.sort(key=custom_key_sort(preferred_llms))
42
+ if not satisfied_models:
43
+ raise ValueError(f"No model found for features: {desired_features}")
44
+ return satisfied_models[0]
45
+
46
+
47
+ FEATURES = {
48
+ LLM.o3_mini: LLMFeatures(
49
+ speed="fast",
50
+ context_size="small",
51
+ model_size="small",
52
+ structured_output=True,
53
+ thinking=True,
54
+ ),
55
+ # FIXME
56
+ }
57
+
58
+ preferred_llms: list[LLMName] = [
59
+ LLM.o3_mini,
60
+ LLM.o1_mini,
61
+ LLM.o1,
62
+ LLM.gpt_4o_mini,
63
+ LLM.gpt_4o,
64
+ LLM.gpt_4,
65
+ LLM.claude_3_7_sonnet,
66
+ LLM.claude_3_5_sonnet,
67
+ LLM.claude_3_5_haiku,
68
+ ]
@@ -10,8 +10,7 @@ from pydantic_core.core_schema import (
10
10
  str_schema,
11
11
  to_string_ser_schema,
12
12
  )
13
-
14
- from kash.utils.common.string_template import StringTemplate
13
+ from strif import StringTemplate
15
14
 
16
15
 
17
16
  class Message(str):
@@ -12,7 +12,7 @@ from pydantic_core.core_schema import (
12
12
  )
13
13
  from rich.text import Text
14
14
 
15
- from kash.config.text_styles import format_success_emoji
15
+ from kash.shell.output.shell_formatting import format_success_emoji
16
16
  from kash.utils.common.type_utils import not_none
17
17
 
18
18
 
kash/llm_utils/llms.py CHANGED
@@ -8,18 +8,23 @@ from kash.llm_utils.llm_names import LLMName
8
8
  class LLM(LLMName, Enum):
9
9
  """
10
10
  Convenience names for common LLMs. This isn't exhaustive, but just common
11
- ones for autocomplete, docs, etc. Values are all LiteLLM names.
11
+ ones for autocomplete, docs, etc. Values are all LiteLLM names. See:
12
+ https://github.com/BerriAI/litellm/blob/main/litellm/model_prices_and_context_window_backup.json
12
13
  """
13
14
 
14
15
  # https://platform.openai.com/docs/models
15
- o3_mini = LLMName("o3-mini")
16
16
  o1_mini = LLMName("o1-mini")
17
17
  o1 = LLMName("o1")
18
+ o3 = LLMName("o3")
19
+ o3_mini = LLMName("o3-mini")
20
+ o4_mini = LLMName("o4-mini")
18
21
  o1_preview = LLMName("o1-preview")
19
22
  gpt_4o_mini = LLMName("gpt-4o-mini")
20
23
  gpt_4o = LLMName("gpt-4o")
21
24
  gpt_4 = LLMName("gpt-4")
22
- gpt_3_5_turbo = LLMName("gpt-3.5-turbo")
25
+ gpt_4_1 = LLMName("gpt-4.1")
26
+ gpt_4_1_mini = LLMName("gpt-4.1-mini")
27
+ gpt_4_1_nano = LLMName("gpt-4.1-nano")
23
28
 
24
29
  # https://docs.anthropic.com/en/docs/about-claude/models/all-models
25
30
  claude_3_7_sonnet = LLMName("claude-3-7-sonnet-latest")
@@ -35,15 +40,6 @@ class LLM(LLMName, Enum):
35
40
  # https://docs.x.ai/docs/models
36
41
  xai_grok_2 = LLMName("xai/grok-2-latest")
37
42
 
38
- # https://docs.mistral.ai/getting-started/models/models_overview/
39
- mistral_small = LLMName("mistral/mistral-small-latest")
40
- mistral_large = LLMName("mistral/mistral-large-latest")
41
- mistral_codestral = LLMName("mistral/mistral-codestral-latest")
42
-
43
- # https://docs.perplexity.ai/guides/model-cards
44
- sonar = LLMName("perplexity/sonar")
45
- sonar_pro = LLMName("perplexity/sonar-pro")
46
-
47
43
  # https://api-docs.deepseek.com/quick_start/pricing
48
44
  deepseek_chat = LLMName("deepseek/deepseek-chat")
49
45
  deepseek_coder = LLMName("deepseek/deepseek-coder")
@@ -55,6 +51,15 @@ class LLM(LLMName, Enum):
55
51
  groq_deepseek_r1_distill_llama_70b = LLMName("groq/deepseek-r1-distill-llama-70b")
56
52
  groq_deepseek_r1_distill_qwen_32b = LLMName("groq/deepseek-r1-distill-qwen-32b")
57
53
 
54
+ # https://docs.perplexity.ai/guides/model-cards
55
+ sonar = LLMName("perplexity/sonar")
56
+ sonar_pro = LLMName("perplexity/sonar-pro")
57
+
58
+ # https://docs.mistral.ai/getting-started/models/models_overview/
59
+ mistral_small = LLMName("mistral/mistral-small-latest")
60
+ mistral_large = LLMName("mistral/mistral-large-latest")
61
+ mistral_codestral = LLMName("mistral/mistral-codestral-latest")
62
+
58
63
  # Allows use of "default_standard" etc as model names and have the
59
64
  # model be looked up from workspace parameter settings.
60
65
  default_standard = LLMName("default_standard")
@@ -1,2 +1,5 @@
1
- # Ensure commands are registered.
2
- import kash.local_server.local_server_commands # noqa: F401
1
+ from pathlib import Path
2
+
3
+ from kash.exec.importing import import_and_register
4
+
5
+ import_and_register(__package__, Path(__file__).parent, ["."])
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import threading
5
5
  from functools import cached_property
6
+ from pathlib import Path
6
7
  from typing import TYPE_CHECKING
7
8
 
8
9
  if TYPE_CHECKING:
@@ -13,7 +14,10 @@ from prettyfmt import fmt_path
13
14
 
14
15
  from kash.config.logger import get_logger
15
16
  from kash.config.server_config import create_server_config
16
- from kash.config.settings import atomic_global_settings, global_settings, server_log_file_path
17
+ from kash.config.settings import (
18
+ atomic_global_settings,
19
+ global_settings,
20
+ )
17
21
  from kash.local_server import local_server_routes
18
22
  from kash.local_server.port_tools import find_available_local_port
19
23
  from kash.utils.errors import InvalidInput, InvalidState
@@ -31,14 +35,14 @@ def _app_setup() -> FastAPI:
31
35
 
32
36
  # Map common exceptions to HTTP codes.
33
37
  # FileNotFound first, since it might also be an InvalidInput.
34
- @app.exception_handler(FileNotFoundError)
38
+ @app.exception_handler(FileNotFoundError) # pyright: ignore[reportUntypedFunctionDecorator]
35
39
  async def file_not_found_exception_handler(_request: Request, exc: FileNotFoundError):
36
40
  return JSONResponse(
37
41
  status_code=404,
38
42
  content={"message": f"File not found: {exc}"},
39
43
  )
40
44
 
41
- @app.exception_handler(InvalidInput)
45
+ @app.exception_handler(InvalidInput) # pyright: ignore[reportUntypedFunctionDecorator]
42
46
  async def invalid_input_exception_handler(_request: Request, exc: InvalidInput):
43
47
  return JSONResponse(
44
48
  status_code=400,
@@ -46,7 +50,7 @@ def _app_setup() -> FastAPI:
46
50
  )
47
51
 
48
52
  # Global exception handler.
49
- @app.exception_handler(Exception)
53
+ @app.exception_handler(Exception) # pyright: ignore[reportUntypedFunctionDecorator]
50
54
  async def global_exception_handler(_request: Request, _exc: Exception):
51
55
  return JSONResponse(
52
56
  status_code=500,
@@ -56,8 +60,8 @@ def _app_setup() -> FastAPI:
56
60
  return app
57
61
 
58
62
 
59
- LOCAL_SERVER_NAME = "local_server"
60
- LOCAL_SERVER_HOST = "127.0.0.1"
63
+ UI_SERVER_NAME = "local_ui_server"
64
+ UI_SERVER_HOST = "127.0.0.1"
61
65
  """
62
66
  The local hostname to run the local server on.
63
67
 
@@ -72,7 +76,7 @@ def _pick_port() -> int:
72
76
  """
73
77
  settings = global_settings()
74
78
  port = find_available_local_port(
75
- LOCAL_SERVER_HOST,
79
+ UI_SERVER_HOST,
76
80
  range(
77
81
  settings.local_server_ports_start,
78
82
  settings.local_server_ports_start + settings.local_server_ports_max,
@@ -86,62 +90,68 @@ def _pick_port() -> int:
86
90
 
87
91
 
88
92
  class LocalServer:
89
- def __init__(self):
93
+ def __init__(self, server_name: str, host: str, log_path: Path):
94
+ self.server_name = server_name
95
+ self.host = host
96
+ self.log_path = log_path
90
97
  self.server_lock = threading.RLock()
91
- self.server_instance: uvicorn.Server | None = None
92
98
  self.did_exit = threading.Event()
99
+ self.server_instance: uvicorn.Server | None = None
100
+ self.port: int
93
101
 
94
102
  @cached_property
95
103
  def app(self) -> FastAPI:
96
104
  return _app_setup()
97
105
 
98
- def _run_server(self):
106
+ @property
107
+ def host_port(self) -> str | None:
108
+ if self.server_instance:
109
+ return f"{self.server_instance.config.host}:{self.server_instance.config.port}"
110
+ else:
111
+ return None
112
+
113
+ def _setup_server(self):
99
114
  import uvicorn
100
115
 
101
116
  port = _pick_port()
102
- self.log_path = server_log_file_path(LOCAL_SERVER_NAME, port)
117
+ self.port = port
118
+ config = create_server_config(self.app, self.host, port, self.server_name, self.log_path)
103
119
 
104
- config = create_server_config(
105
- self.app, LOCAL_SERVER_HOST, port, LOCAL_SERVER_NAME, self.log_path
106
- )
107
- with self.server_lock:
108
- server = uvicorn.Server(config)
109
- self.server_instance = server
110
-
111
- async def serve():
112
- try:
113
- log.message(
114
- "Starting local server on %s:%s",
115
- LOCAL_SERVER_HOST,
116
- port,
117
- )
118
- log.message("Local server logs: %s", fmt_path(server_log_file_path(port)))
119
- await server.serve()
120
- finally:
121
- self.did_exit.set()
120
+ server = uvicorn.Server(config)
121
+ self.server_instance = server
122
122
 
123
+ def _run_server_thread(self):
124
+ assert self.server_instance
123
125
  try:
124
- asyncio.run(serve())
126
+ asyncio.run(self.server_instance.serve())
125
127
  except Exception as e:
126
128
  log.error("Server failed with error: %s", e)
127
129
  finally:
128
- with self.server_lock:
129
- self.server_instance = None
130
+ self.server_instance = None
131
+ self.did_exit.set()
130
132
 
131
133
  def start_server(self):
132
134
  with self.server_lock:
133
135
  if self.server_instance:
134
136
  log.warning(
135
- "Server already running on %s:%s.",
136
- self.server_instance.config.host,
137
- self.server_instance.config.port,
137
+ "Server already running on: %s",
138
+ self.host_port,
138
139
  )
139
140
  return
140
141
 
141
142
  self.did_exit.clear()
142
- server_thread = threading.Thread(target=self._run_server, daemon=True)
143
+
144
+ self._setup_server()
145
+
146
+ server_thread = threading.Thread(target=self._run_server_thread, daemon=True)
143
147
  server_thread.start()
144
148
  log.info("Created new local server thread: %s", server_thread)
149
+ log.message(
150
+ "Started server %s on %s with logs to %s",
151
+ UI_SERVER_NAME,
152
+ self.host_port,
153
+ fmt_path(self.log_path),
154
+ )
145
155
 
146
156
  def stop_server(self):
147
157
  with self.server_lock:
@@ -159,25 +169,25 @@ class LocalServer:
159
169
  raise InvalidState(f"Server did not shut down within {timeout} seconds")
160
170
 
161
171
  self.server_instance = None
162
- log.warning("Server stopped.")
172
+ log.warning("Stopped server %s", UI_SERVER_NAME)
163
173
 
164
174
  def restart_server(self):
165
175
  self.stop_server()
166
176
  self.start_server()
167
177
 
168
178
 
169
- # Singleton instance.
170
- # Note this is quick to set up (lazy import).
171
- _local_server = LocalServer()
179
+ # Singleton instance for the UI server.
180
+ # Note this is quick to set up (lazy imports).
181
+ _ui_server = LocalServer(UI_SERVER_NAME, UI_SERVER_HOST, global_settings().local_server_log_path)
172
182
 
173
183
 
174
- def start_local_server():
175
- _local_server.start_server()
184
+ def start_ui_server():
185
+ _ui_server.start_server()
176
186
 
177
187
 
178
- def stop_local_server():
179
- _local_server.stop_server()
188
+ def stop_ui_server():
189
+ _ui_server.stop_server()
180
190
 
181
191
 
182
- def restart_local_server():
183
- _local_server.restart_server()
192
+ def restart_ui_server():
193
+ _ui_server.restart_server()