Skip to content

Commit 112fd66

Browse files
committed
Make llm optional.
1 parent db28530 commit 112fd66

5 files changed

Lines changed: 80 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
### Features
4+
5+
- Make LLM support optional and installable via `litecli[ai]`.
6+
17
## 1.18.0
28

39
### Internal

litecli/packages/special/llm.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,22 @@
1313
from typing import Any
1414

1515
import click
16-
import llm
17-
from llm.cli import cli
16+
17+
try:
18+
import llm
19+
20+
LLM_IMPORTED = True
21+
except ImportError:
22+
llm = None
23+
LLM_IMPORTED = False
24+
25+
try:
26+
from llm.cli import cli
27+
28+
LLM_CLI_IMPORTED = True
29+
except ImportError:
30+
cli = None
31+
LLM_CLI_IMPORTED = False
1832

1933
from . import export
2034
from .main import Verbosity, parse_special_command
@@ -23,10 +37,10 @@
2337
log = logging.getLogger(__name__)
2438

2539
LLM_TEMPLATE_NAME = "litecli-llm-template"
26-
LLM_CLI_COMMANDS: list[str] = list(cli.commands.keys())
40+
LLM_CLI_COMMANDS: list[str] = list(cli.commands.keys()) if LLM_CLI_IMPORTED else []
2741
# Mapping of model_id to None used for completion tree leaves.
2842
# 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]
43+
MODELS: dict[str, None] = {x.model_id: None for x in llm.get_models()} if LLM_IMPORTED else {} # type: ignore[attr-defined]
3044

3145

3246
def run_external_cmd(
@@ -110,7 +124,7 @@ def build_command_tree(cmd: click.Command) -> dict[str, Any] | None:
110124

111125

112126
# Generate the tree
113-
COMMAND_TREE: dict[str, Any] | None = build_command_tree(cli)
127+
COMMAND_TREE: dict[str, Any] | None = build_command_tree(cli) if LLM_CLI_IMPORTED else {}
114128

115129

116130
def get_completions(tokens: list[str], tree: dict[str, Any] | None = COMMAND_TREE) -> list[str]:
@@ -123,6 +137,8 @@ def get_completions(tokens: list[str], tree: dict[str, Any] | None = COMMAND_TRE
123137
Returns:
124138
list[str]: List of possible completions.
125139
"""
140+
if not LLM_CLI_IMPORTED:
141+
return []
126142
for token in tokens:
127143
if token.startswith("-"):
128144
# Skip options (flags)
@@ -171,6 +187,18 @@ def __init__(self, results: Any | None = None) -> None:
171187
# https://llm.datasette.io/en/stable/plugins/directory.html
172188
"""
173189

190+
NEED_DEPENDENCIES = """
191+
To enable LLM features you need to install litecli with AI support:
192+
193+
pip install 'litecli[ai]'
194+
195+
or install LLM libraries separately
196+
197+
pip install llm
198+
199+
This is required to use the \\llm command.
200+
"""
201+
174202
_SQL_CODE_FENCE = r"```sql\n(.*?)\n```"
175203
PROMPT = """
176204
You are a helpful assistant who is a SQLite expert. You are embedded in a SQLite
@@ -230,6 +258,10 @@ def handle_llm(text: str, cur: DBCursor) -> tuple[str, str | None, float]:
230258
is_verbose = mode is Verbosity.VERBOSE
231259
is_succinct = mode is Verbosity.SUCCINCT
232260

261+
if not LLM_IMPORTED:
262+
output = [(None, None, None, NEED_DEPENDENCIES)]
263+
raise FinishIteration(output)
264+
233265
if not arg.strip(): # No question provided. Print usage and bail.
234266
output = [(None, None, None, USAGE)]
235267
raise FinishIteration(output)

litecli/packages/special/main.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
log = logging.getLogger(__name__)
1010

11+
try:
12+
import llm # noqa: F401
13+
14+
LLM_IMPORTED = True
15+
except ImportError:
16+
LLM_IMPORTED = False
17+
1118
NO_QUERY = 0
1219
PARSED_QUERY = 1
1320
RAW_QUERY = 2
@@ -176,13 +183,19 @@ def quit(*_args: Any) -> None:
176183
arg_type=NO_QUERY,
177184
case_sensitive=True,
178185
)
179-
@special_command(
180-
"\\llm",
181-
"\\ai",
182-
"Use LLM to construct a SQL query.",
183-
arg_type=NO_QUERY,
184-
case_sensitive=False,
185-
aliases=(".ai", ".llm"),
186-
)
187186
def stub() -> None:
188187
raise NotImplementedError
188+
189+
190+
if LLM_IMPORTED:
191+
192+
@special_command(
193+
"\\llm",
194+
"\\ai",
195+
"Use LLM to construct a SQL query.",
196+
arg_type=NO_QUERY,
197+
case_sensitive=False,
198+
aliases=(".ai", ".llm"),
199+
)
200+
def llm_stub() -> None:
201+
raise NotImplementedError

pyproject.toml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ dependencies = [
1313
"configobj>=5.0.5",
1414
"prompt-toolkit>=3.0.3,<4.0.0",
1515
"pygments>=1.6",
16-
"sqlparse>=0.4.4",
17-
"setuptools", # Required by llm commands to install models
18-
"pip",
19-
"llm>=0.25.0"
16+
"sqlparse>=0.4.4"
2017
]
2118

2219
[build-system]
@@ -33,7 +30,11 @@ build-backend = "setuptools.build_meta"
3330
litecli = "litecli.main:cli"
3431

3532
[project.optional-dependencies]
36-
ai = ["llm"]
33+
ai = [
34+
"llm>=0.25.0",
35+
"setuptools", # Required by llm commands to install models
36+
"pip",
37+
]
3738
sqlean = ["sqlean-py>=3.47.0",
3839
"sqlean-stubs>=0.0.3"]
3940

@@ -45,7 +46,9 @@ dev = [
4546
"pytest-cov>=4.1.0",
4647
"tox>=4.8.0",
4748
"pdbpp>=0.10.3",
48-
"llm>=0.19.0",
49+
"llm>=0.25.0",
50+
"setuptools",
51+
"pip",
4952
"ty>=0.0.4"
5053
]
5154

tests/test_llm_special.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
import pytest
44

5+
import litecli.packages.special.llm as llm_module
56
from litecli.packages.special.llm import USAGE, FinishIteration, handle_llm
67

78

9+
@pytest.fixture(autouse=True)
10+
def enable_llm(monkeypatch):
11+
monkeypatch.setattr(llm_module, "LLM_IMPORTED", True)
12+
monkeypatch.setattr(llm_module, "LLM_CLI_COMMANDS", ["models"])
13+
14+
815
@patch("litecli.packages.special.llm.llm")
916
def test_llm_command_without_args(mock_llm, executor):
1017
r"""

0 commit comments

Comments
 (0)