11from __future__ import annotations
22
33import contextlib
4+ import importlib
45import io
56import logging
67import os
1314from typing import Any
1415
1516import click
16- import llm
17- from llm .cli import cli
1817
1918from . import export
2019from .main import Verbosity , parse_special_command
2120from .types import DBCursor
2221
22+
23+ def _load_llm_module () -> Any | None :
24+ try :
25+ return importlib .import_module ("llm" )
26+ except ImportError :
27+ return None
28+
29+
30+ def _load_llm_cli_module () -> Any | None :
31+ try :
32+ return importlib .import_module ("llm.cli" )
33+ except ImportError :
34+ return None
35+
36+
37+ llm_module = _load_llm_module ()
38+ llm_cli_module = _load_llm_cli_module ()
39+
40+ # Alias for tests and patching.
41+ llm = llm_module
42+
43+ LLM_IMPORTED = llm_module is not None
44+
45+ cli : click .Command | None
46+ if llm_cli_module is not None :
47+ llm_cli = getattr (llm_cli_module , "cli" , None )
48+ cli = llm_cli if isinstance (llm_cli , click .Command ) else None
49+ else :
50+ cli = None
51+
52+ LLM_CLI_IMPORTED = cli is not None
53+
2354log = logging .getLogger (__name__ )
2455
2556LLM_TEMPLATE_NAME = "litecli-llm-template"
26- LLM_CLI_COMMANDS : list [str ] = list (cli .commands .keys ())
57+ LLM_CLI_COMMANDS : list [str ] = list (cli .commands .keys ()) if isinstance ( cli , click . Group ) else []
2758# Mapping of model_id to None used for completion tree leaves.
28- # the file name is llm.py and module name is llm, hence ty is complaining that get_models is missing.
29- MODELS : dict [str , None ] = {x .model_id : None for x in llm .get_models ()} # type: ignore[attr-defined]
59+ if llm_module is not None :
60+ get_models = getattr (llm_module , "get_models" , None )
61+ MODELS : dict [str , None ] = {x .model_id : None for x in get_models ()} if callable (get_models ) else {}
62+ else :
63+ MODELS = {}
3064
3165
3266def run_external_cmd (
@@ -110,7 +144,7 @@ def build_command_tree(cmd: click.Command) -> dict[str, Any] | None:
110144
111145
112146# Generate the tree
113- COMMAND_TREE : dict [str , Any ] | None = build_command_tree (cli )
147+ COMMAND_TREE : dict [str , Any ] | None = build_command_tree (cli ) if cli is not None else {}
114148
115149
116150def get_completions (tokens : list [str ], tree : dict [str , Any ] | None = COMMAND_TREE ) -> list [str ]:
@@ -123,6 +157,8 @@ def get_completions(tokens: list[str], tree: dict[str, Any] | None = COMMAND_TRE
123157 Returns:
124158 list[str]: List of possible completions.
125159 """
160+ if not LLM_CLI_IMPORTED :
161+ return []
126162 for token in tokens :
127163 if token .startswith ("-" ):
128164 # Skip options (flags)
@@ -171,6 +207,18 @@ def __init__(self, results: Any | None = None) -> None:
171207# https://llm.datasette.io/en/stable/plugins/directory.html
172208"""
173209
210+ NEED_DEPENDENCIES = """
211+ To enable LLM features you need to install litecli with AI support:
212+
213+ pip install 'litecli[ai]'
214+
215+ or install LLM libraries separately
216+
217+ pip install llm
218+
219+ This is required to use the \\ llm command.
220+ """
221+
174222_SQL_CODE_FENCE = r"```sql\n(.*?)\n```"
175223PROMPT = """
176224You are a helpful assistant who is a SQLite expert. You are embedded in a SQLite
@@ -230,6 +278,10 @@ def handle_llm(text: str, cur: DBCursor) -> tuple[str, str | None, float]:
230278 is_verbose = mode is Verbosity .VERBOSE
231279 is_succinct = mode is Verbosity .SUCCINCT
232280
281+ if not LLM_IMPORTED :
282+ output = [(None , None , None , NEED_DEPENDENCIES )]
283+ raise FinishIteration (output )
284+
233285 if not arg .strip (): # No question provided. Print usage and bail.
234286 output = [(None , None , None , USAGE )]
235287 raise FinishIteration (output )
0 commit comments