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.
- kash/actions/__init__.py +4 -4
- kash/actions/core/markdownify.py +5 -2
- kash/actions/core/readability.py +5 -2
- kash/actions/core/render_as_html.py +18 -0
- kash/actions/core/webpage_config.py +12 -4
- kash/commands/__init__.py +8 -20
- kash/commands/base/basic_file_commands.py +15 -0
- kash/commands/base/debug_commands.py +15 -2
- kash/commands/base/general_commands.py +27 -18
- kash/commands/base/logs_commands.py +1 -4
- kash/commands/base/model_commands.py +8 -8
- kash/commands/base/search_command.py +3 -2
- kash/commands/base/show_command.py +5 -3
- kash/commands/extras/parse_uv_lock.py +186 -0
- kash/commands/help/doc_commands.py +2 -31
- kash/commands/help/welcome.py +33 -0
- kash/commands/workspace/selection_commands.py +11 -6
- kash/commands/workspace/workspace_commands.py +19 -16
- kash/config/colors.py +2 -0
- kash/config/env_settings.py +72 -0
- kash/config/init.py +2 -2
- kash/config/logger.py +61 -59
- kash/config/logger_basic.py +12 -5
- kash/config/server_config.py +6 -6
- kash/config/settings.py +117 -67
- kash/config/setup.py +35 -9
- kash/config/suppress_warnings.py +30 -12
- kash/config/text_styles.py +3 -13
- kash/docs/load_api_docs.py +2 -1
- kash/docs/markdown/topics/a2_installation.md +7 -3
- kash/docs/markdown/topics/a3_getting_started.md +3 -2
- kash/docs/markdown/warning.md +3 -8
- kash/docs/markdown/welcome.md +4 -0
- kash/docs_base/load_recipe_snippets.py +1 -1
- kash/docs_base/recipes/{general_system_commands.ksh → general_system_commands.sh} +1 -1
- kash/{concepts → embeddings}/cosine.py +2 -1
- kash/embeddings/text_similarity.py +57 -0
- kash/exec/__init__.py +20 -3
- kash/exec/action_decorators.py +18 -4
- kash/exec/action_exec.py +41 -23
- kash/exec/action_registry.py +13 -48
- kash/exec/command_registry.py +2 -1
- kash/exec/fetch_url_metadata.py +4 -6
- kash/exec/importing.py +56 -0
- kash/exec/llm_transforms.py +6 -6
- kash/exec/precondition_registry.py +2 -1
- kash/exec/preconditions.py +16 -1
- kash/exec/shell_callable_action.py +33 -19
- kash/file_storage/file_store.py +23 -14
- kash/file_storage/item_file_format.py +13 -3
- kash/file_storage/metadata_dirs.py +11 -2
- kash/help/assistant.py +2 -2
- kash/help/assistant_instructions.py +2 -1
- kash/help/help_embeddings.py +2 -2
- kash/help/help_printing.py +14 -10
- kash/help/tldr_help.py +5 -3
- kash/llm_utils/clean_headings.py +1 -1
- kash/llm_utils/llm_api_keys.py +4 -4
- kash/llm_utils/llm_completion.py +2 -2
- kash/llm_utils/llm_features.py +68 -0
- kash/llm_utils/llm_messages.py +1 -2
- kash/llm_utils/llm_names.py +1 -1
- kash/llm_utils/llms.py +17 -12
- kash/local_server/__init__.py +5 -2
- kash/local_server/local_server.py +56 -46
- kash/local_server/local_server_commands.py +15 -15
- kash/local_server/local_server_routes.py +2 -2
- kash/local_server/local_url_formatters.py +1 -1
- kash/mcp/__init__.py +5 -2
- kash/mcp/mcp_cli.py +54 -17
- kash/mcp/mcp_server_commands.py +5 -6
- kash/mcp/mcp_server_routes.py +14 -11
- kash/mcp/mcp_server_sse.py +61 -34
- kash/mcp/mcp_server_stdio.py +0 -8
- kash/media_base/audio_processing.py +81 -7
- kash/media_base/media_cache.py +18 -18
- kash/media_base/media_services.py +1 -1
- kash/media_base/media_tools.py +6 -6
- kash/media_base/services/local_file_media.py +2 -2
- kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -109
- kash/media_base/transcription_format.py +73 -0
- kash/media_base/transcription_whisper.py +38 -0
- kash/model/__init__.py +73 -5
- kash/model/actions_model.py +38 -4
- kash/model/concept_model.py +30 -0
- kash/model/items_model.py +56 -13
- kash/model/params_model.py +24 -0
- kash/shell/completions/completion_scoring.py +37 -5
- kash/shell/output/kerm_codes.py +1 -2
- kash/shell/output/shell_formatting.py +14 -4
- kash/shell/shell_main.py +2 -2
- kash/shell/utils/exception_printing.py +6 -0
- kash/shell/utils/native_utils.py +26 -20
- kash/text_handling/custom_sliding_transforms.py +12 -4
- kash/text_handling/doc_normalization.py +6 -2
- kash/text_handling/markdown_render.py +117 -0
- kash/text_handling/markdown_utils.py +204 -0
- kash/utils/common/import_utils.py +12 -3
- kash/utils/common/type_utils.py +0 -29
- kash/utils/common/url.py +80 -28
- kash/utils/errors.py +6 -0
- kash/utils/file_utils/{dir_size.py → dir_info.py} +25 -4
- kash/utils/file_utils/file_ext.py +2 -3
- kash/utils/file_utils/file_formats.py +28 -2
- kash/utils/file_utils/file_formats_model.py +50 -19
- kash/utils/file_utils/filename_parsing.py +10 -4
- kash/web_content/dir_store.py +1 -2
- kash/web_content/file_cache_utils.py +37 -10
- kash/web_content/file_processing.py +68 -0
- kash/web_content/local_file_cache.py +12 -9
- kash/web_content/web_extract.py +8 -3
- kash/web_content/web_fetch.py +12 -4
- kash/web_gen/tabbed_webpage.py +5 -2
- kash/web_gen/templates/base_styles.css.jinja +120 -14
- kash/web_gen/templates/base_webpage.html.jinja +60 -13
- kash/web_gen/templates/content_styles.css.jinja +4 -2
- kash/web_gen/templates/item_view.html.jinja +2 -2
- kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
- kash/workspaces/__init__.py +15 -2
- kash/workspaces/selections.py +18 -3
- kash/workspaces/source_items.py +4 -2
- kash/workspaces/workspace_output.py +11 -4
- kash/workspaces/workspaces.py +5 -11
- kash/xonsh_custom/command_nl_utils.py +40 -19
- kash/xonsh_custom/custom_shell.py +44 -12
- kash/xonsh_custom/customize_prompt.py +39 -21
- kash/xonsh_custom/load_into_xonsh.py +26 -27
- kash/xonsh_custom/shell_load_commands.py +2 -2
- kash/xonsh_custom/xonsh_completers.py +2 -249
- kash/xonsh_custom/xonsh_keybindings.py +282 -0
- kash/xonsh_custom/xonsh_modern_tools.py +3 -3
- kash/xontrib/kash_extension.py +5 -6
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/METADATA +26 -12
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/RECORD +140 -140
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +1 -1
- kash/concepts/concept_formats.py +0 -23
- kash/concepts/text_similarity.py +0 -112
- kash/shell/clideps/api_keys.py +0 -99
- kash/shell/clideps/dotenv_setup.py +0 -114
- kash/shell/clideps/dotenv_utils.py +0 -89
- kash/shell/clideps/pkg_deps.py +0 -232
- kash/shell/clideps/platforms.py +0 -11
- kash/shell/clideps/terminal_features.py +0 -56
- kash/shell/utils/osc_utils.py +0 -95
- kash/shell/utils/terminal_images.py +0 -133
- kash/text_handling/markdown_util.py +0 -167
- kash/utils/common/atomic_var.py +0 -158
- kash/utils/common/string_replace.py +0 -93
- kash/utils/common/string_template.py +0 -101
- /kash/docs_base/recipes/{python_dev_commands.ksh → python_dev_commands.sh} +0 -0
- /kash/docs_base/recipes/{tldr_standard_commands.ksh → tldr_standard_commands.sh} +0 -0
- /kash/{concepts → embeddings}/embeddings.py +0 -0
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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 [
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
kash/help/help_embeddings.py
CHANGED
|
@@ -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
|
|
kash/help/help_printing.py
CHANGED
|
@@ -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,
|
|
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(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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,
|
|
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
|
|
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",
|
|
298
|
+
"Dumped %s TLDR snippets: %s",
|
|
299
|
+
len(commands),
|
|
300
|
+
RECIPES_DIR / ("tldr_standard_commands" + RECIPE_EXT),
|
|
299
301
|
)
|
kash/llm_utils/clean_headings.py
CHANGED
|
@@ -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.
|
|
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
|
|
kash/llm_utils/llm_api_keys.py
CHANGED
|
@@ -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) ->
|
|
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
|
|
21
|
+
return EnvName.api_env_name(custom_llm_provider)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def have_key_for_model(model: LLMName) -> bool:
|
kash/llm_utils/llm_completion.py
CHANGED
|
@@ -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 =
|
|
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
|
+
]
|
kash/llm_utils/llm_messages.py
CHANGED
kash/llm_utils/llm_names.py
CHANGED
|
@@ -12,7 +12,7 @@ from pydantic_core.core_schema import (
|
|
|
12
12
|
)
|
|
13
13
|
from rich.text import Text
|
|
14
14
|
|
|
15
|
-
from kash.
|
|
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
|
-
|
|
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")
|
kash/local_server/__init__.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
117
|
+
self.port = port
|
|
118
|
+
config = create_server_config(self.app, self.host, port, self.server_name, self.log_path)
|
|
103
119
|
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
136
|
-
self.
|
|
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
|
-
|
|
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("
|
|
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
|
|
171
|
-
|
|
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
|
|
175
|
-
|
|
184
|
+
def start_ui_server():
|
|
185
|
+
_ui_server.start_server()
|
|
176
186
|
|
|
177
187
|
|
|
178
|
-
def
|
|
179
|
-
|
|
188
|
+
def stop_ui_server():
|
|
189
|
+
_ui_server.stop_server()
|
|
180
190
|
|
|
181
191
|
|
|
182
|
-
def
|
|
183
|
-
|
|
192
|
+
def restart_ui_server():
|
|
193
|
+
_ui_server.restart_server()
|