From 32efa7b2c6b4256f30edb1c56d9ea094afdd96c6 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 4 May 2025 20:56:10 -0700 Subject: [PATCH 1/8] Rename succinct to nocontext. --- litecli/packages/special/llm.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/litecli/packages/special/llm.py b/litecli/packages/special/llm.py index 956d659..33a7b9e 100644 --- a/litecli/packages/special/llm.py +++ b/litecli/packages/special/llm.py @@ -28,8 +28,10 @@ log = logging.getLogger(__name__) +LLM_TEMPLATE_NAME = "litecli-llm-template" -def run_external_cmd(cmd, *args, capture_output=False, restart_cli=False, raise_exception=True): + +def run_external_cmd(cmd, *args, capture_output=False, restart_cli=False, raise_exception=True) -> Tuple[int, str]: original_exe = sys.executable original_args = sys.argv @@ -55,6 +57,13 @@ def run_external_cmd(cmd, *args, capture_output=False, restart_cli=False, raise_ raise RuntimeError(buffer.getvalue()) else: raise RuntimeError(f"Command {cmd} failed with exit code {code}.") + except Exception as e: + code = 1 + if raise_exception: + if capture_output: + raise RuntimeError(buffer.getvalue()) + else: + raise RuntimeError(f"Command {cmd} failed: {e}") if restart_cli and code == 0: os.execv(original_exe, [original_exe] + original_args) @@ -171,6 +180,9 @@ def __init__(self, results=None): ```sql SELECT count(*) FROM table_name; ``` + +If the question cannot be answered based on the database schema respond with "I +cannot answer that question" in a sql code fence. """ @@ -187,11 +199,11 @@ def ensure_litecli_template(replace=False): """ if not replace: # Check if it already exists. - code, _ = run_external_cmd("llm", "templates", "show", "litecli", capture_output=True, raise_exception=False) + code, _ = run_external_cmd("llm", "templates", "show", LLM_TEMPLATE_NAME, capture_output=True, raise_exception=False) if code == 0: # Template already exists. No need to create it. return - run_external_cmd("llm", PROMPT, "--save", "litecli") + run_external_cmd("llm", PROMPT, "--save", LLM_TEMPLATE_NAME) return @@ -205,7 +217,7 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: FinishIteration() which will be caught by the main loop AND print any output that was supplied (or None). """ - _, verbose, arg = parse_special_command(text) + _, nocontenxt, arg = parse_special_command(text) # LLM is not installed. if llm is None: @@ -268,7 +280,7 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: output = [(None, None, None, result)] raise FinishIteration(output) - return result if verbose else "", sql, end - start + return ("" if nocontenxt else result), sql, end - start else: run_external_cmd("llm", *args, restart_cli=restart) raise FinishIteration(None) @@ -278,9 +290,9 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: # Measure end to end llm command invocation. # This measures the internal DB command to pull the schema and llm command start = time() - context, sql = sql_using_llm(cur=cur, question=arg, verbose=verbose) + context, sql = sql_using_llm(cur=cur, question=arg) end = time() - if not verbose: + if nocontenxt: context = "" return context, sql, end - start except Exception as e: @@ -298,7 +310,7 @@ def is_llm_command(command) -> bool: @export -def sql_using_llm(cur, question=None, verbose=False) -> Tuple[str, Optional[str]]: +def sql_using_llm(cur, question=None) -> Tuple[str, Optional[str]]: if cur is None: raise RuntimeError("Connect to a datbase and try again.") schema_query = """ @@ -331,7 +343,7 @@ def sql_using_llm(cur, question=None, verbose=False) -> Tuple[str, Optional[str] args = [ "--template", - "litecli", + LLM_TEMPLATE_NAME, "--param", "db_schema", db_schema, From 8432311e7e64e040e13e1353b9f30d1e5b261785 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 25 May 2025 12:53:29 -0700 Subject: [PATCH 2/8] Modify \llm to use either + or - modifiers. --- litecli/packages/completion_engine.py | 2 +- litecli/packages/special/llm.py | 51 +++++++++++++++++++-------- litecli/packages/special/main.py | 33 +++++++++++++---- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/litecli/packages/completion_engine.py b/litecli/packages/completion_engine.py index 2d9a033..adecd20 100644 --- a/litecli/packages/completion_engine.py +++ b/litecli/packages/completion_engine.py @@ -86,7 +86,7 @@ def suggest_type(full_text, text_before_cursor): def suggest_special(text): text = text.lstrip() - cmd, _, arg = parse_special_command(text) + cmd, mode, arg = parse_special_command(text) if cmd == text: # Trying to complete the special command itself diff --git a/litecli/packages/special/llm.py b/litecli/packages/special/llm.py index 33a7b9e..1dd6559 100644 --- a/litecli/packages/special/llm.py +++ b/litecli/packages/special/llm.py @@ -10,6 +10,7 @@ from time import time import click +import pprint try: import llm @@ -24,7 +25,7 @@ MODELS = {} from . import export -from .main import parse_special_command +from .main import parse_special_command, CommandMode log = logging.getLogger(__name__) @@ -217,7 +218,10 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: FinishIteration() which will be caught by the main loop AND print any output that was supplied (or None). """ - _, nocontenxt, arg = parse_special_command(text) + # Determine invocation mode: regular, verbose (+), or succinct (-) + _, mode, arg = parse_special_command(text) + is_verbose = mode is CommandMode.VERBOSE + is_succinct = mode is CommandMode.SUCCINCT # LLM is not installed. if llm is None: @@ -280,20 +284,30 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: output = [(None, None, None, result)] raise FinishIteration(output) - return ("" if nocontenxt else result), sql, end - start + # In succinct mode without verbose, suppress context + context = "" if (is_succinct and not is_verbose) else result + return context, sql, end - start else: run_external_cmd("llm", *args, restart_cli=restart) raise FinishIteration(None) try: ensure_litecli_template() - # Measure end to end llm command invocation. - # This measures the internal DB command to pull the schema and llm command + # Measure end-to-end LLM command invocation (schema gathering and LLM call) start = time() - context, sql = sql_using_llm(cur=cur, question=arg) + if is_verbose: + context, sql, prompt = sql_using_llm(cur=cur, question=arg, verbose=True) + else: + context, sql, _ = sql_using_llm(cur=cur, question=arg) end = time() - if nocontenxt: + # Suppress context in succinct mode (unless verbose) + if is_succinct and not is_verbose: context = "" + # In verbose mode, show the prompt sent to the LLM + if is_verbose: + click.echo("LLM Prompt:") + click.echo(prompt) + click.echo("---") return context, sql, end - start except Exception as e: # Something went wrong. Raise an exception and bail. @@ -305,12 +319,12 @@ def is_llm_command(command) -> bool: """ Is this an llm/ai command? """ - cmd, _, _ = parse_special_command(command) + cmd, mode, arg = parse_special_command(command) return cmd in ("\\llm", "\\ai", ".llm", ".ai") @export -def sql_using_llm(cur, question=None) -> Tuple[str, Optional[str]]: +def sql_using_llm(cur, question=None, verbose=False) -> Tuple[str, Optional[str], Optional[str]]: if cur is None: raise RuntimeError("Connect to a datbase and try again.") schema_query = """ @@ -359,9 +373,16 @@ def sql_using_llm(cur, question=None) -> Tuple[str, Optional[str]]: _, result = run_external_cmd("llm", *args, capture_output=True) click.echo("Received response from the llm command") match = re.search(_SQL_CODE_FENCE, result, re.DOTALL) - if match: - sql = match.group(1).strip() - else: - sql = "" - - return result, sql + sql = match.group(1).strip() if match else "" + + # When verbose, build and return the rendered prompt text + prompt_text = None + if verbose: + # Render the prompt by substituting schema, sample_data, and question + prompt_text = PROMPT + prompt_text = prompt_text.replace("$db_schema", db_schema) + prompt_text = prompt_text.replace("$sample_data", pprint.pformat(sample_data)) + prompt_text = prompt_text.replace("$question", question or "") + if verbose: + return result, sql, prompt_text + return result, sql, None diff --git a/litecli/packages/special/main.py b/litecli/packages/special/main.py index 9544811..f2c34a3 100644 --- a/litecli/packages/special/main.py +++ b/litecli/packages/special/main.py @@ -3,6 +3,7 @@ from collections import namedtuple from . import export +from enum import Enum log = logging.getLogger(__name__) @@ -36,12 +37,32 @@ class CommandNotFound(Exception): pass +class CommandMode(Enum): + """Mode for special command invocation: regular, verbose (+), or succinct (-).""" + + REGULAR = "regular" + VERBOSE = "verbose" + SUCCINCT = "succinct" + + @export def parse_special_command(sql): - command, _, arg = sql.partition(" ") - verbose = "+" in command - command = command.strip().replace("+", "") - return (command, verbose, arg.strip()) + """ + Parse a special command prefix, extracting the base command name, + an invocation mode (regular, verbose, or succinct), and the argument. + """ + raw, _, arg = sql.partition(" ") + is_verbose = raw.endswith("+") + is_succinct = raw.endswith("-") + # strip out any + or - modifiers to get the actual command name + command = raw.strip().rstrip("+-") + if is_verbose: + mode = CommandMode.VERBOSE + elif is_succinct: + mode = CommandMode.SUCCINCT + else: + mode = CommandMode.REGULAR + return (command, mode, arg.strip()) @export @@ -101,7 +122,7 @@ def execute(cur, sql): """Execute a special command and return the results. If the special command is not supported a KeyError will be raised. """ - command, verbose, arg = parse_special_command(sql) + command, mode, arg = parse_special_command(sql) if (command not in COMMANDS) and (command.lower() not in COMMANDS): raise CommandNotFound @@ -116,7 +137,7 @@ def execute(cur, sql): if special_cmd.arg_type == NO_QUERY: return special_cmd.handler() elif special_cmd.arg_type == PARSED_QUERY: - return special_cmd.handler(cur=cur, arg=arg, verbose=verbose) + return special_cmd.handler(cur=cur, arg=arg, verbose=(mode is CommandMode.VERBOSE)) elif special_cmd.arg_type == RAW_QUERY: return special_cmd.handler(cur=cur, query=sql) From 34543574aac657a3e6d6d1d66128602ba1532905 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 25 May 2025 13:21:41 -0700 Subject: [PATCH 3/8] Fix up the `\llm` verbose, regular and succinct modes. --- litecli/packages/special/llm.py | 17 +++++------------ tests/test_llm_special.py | 17 +++++++---------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/litecli/packages/special/llm.py b/litecli/packages/special/llm.py index 1dd6559..1d46588 100644 --- a/litecli/packages/special/llm.py +++ b/litecli/packages/special/llm.py @@ -284,8 +284,7 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: output = [(None, None, None, result)] raise FinishIteration(output) - # In succinct mode without verbose, suppress context - context = "" if (is_succinct and not is_verbose) else result + context = "" if is_succinct else result return context, sql, end - start else: run_external_cmd("llm", *args, restart_cli=restart) @@ -295,18 +294,12 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: ensure_litecli_template() # Measure end-to-end LLM command invocation (schema gathering and LLM call) start = time() - if is_verbose: - context, sql, prompt = sql_using_llm(cur=cur, question=arg, verbose=True) - else: - context, sql, _ = sql_using_llm(cur=cur, question=arg) + result, sql, prompt_text = sql_using_llm(cur=cur, question=arg, verbose=is_verbose) end = time() - # Suppress context in succinct mode (unless verbose) - if is_succinct and not is_verbose: - context = "" - # In verbose mode, show the prompt sent to the LLM - if is_verbose: + context = "" if is_succinct else result + if is_verbose and prompt_text is not None: click.echo("LLM Prompt:") - click.echo(prompt) + click.echo(prompt_text) click.echo("---") return context, sql, end - start except Exception as e: diff --git a/tests/test_llm_special.py b/tests/test_llm_special.py index 9de29df..034592b 100644 --- a/tests/test_llm_special.py +++ b/tests/test_llm_special.py @@ -61,11 +61,8 @@ def test_llm_command_with_c_flag_and_fenced_sql(mock_run_cmd, mock_llm, executor result, sql, duration = handle_llm(test_text, executor) - # We expect the function to return (result, sql), but result might be "" if verbose is not set - # By default, `verbose` is false unless text has something like \llm --verbose? - # The function code: return result if verbose else "", sql - # Our test_text doesn't set verbose => we expect "" for the returned context. - assert result == "" + # In regular mode, context is returned + assert result == return_text assert sql == "SELECT * FROM table;" assert isinstance(duration, float) @@ -133,7 +130,7 @@ def test_llm_command_with_prompt(mock_sql_using_llm, mock_ensure_template, mock_ Should use context, capture output, and call sql_using_llm. """ # Mock out the return from sql_using_llm - mock_sql_using_llm.return_value = ("context from LLM", "SELECT 1;") + mock_sql_using_llm.return_value = ("context from LLM", "SELECT 1;", None) test_text = r"\llm prompt 'Magic happening here?'" context, sql, duration = handle_llm(test_text, executor) @@ -144,7 +141,7 @@ def test_llm_command_with_prompt(mock_sql_using_llm, mock_ensure_template, mock_ # Actually, the question is the entire "prompt 'Magic happening here?'" minus the \llm # But in the function we do parse shlex.split. mock_sql_using_llm.assert_called() - assert context == "" + assert context == "context from LLM" assert sql == "SELECT 1;" assert isinstance(duration, float) @@ -156,14 +153,14 @@ def test_llm_command_question_with_context(mock_sql_using_llm, mock_ensure_templ """ If arg doesn't contain any known command, it's treated as a question => capture output + context. """ - mock_sql_using_llm.return_value = ("You have context!", "SELECT 2;") + mock_sql_using_llm.return_value = ("You have context!", "SELECT 2;", None) test_text = r"\llm 'Top 10 downloads by size.'" context, sql, duration = handle_llm(test_text, executor) mock_ensure_template.assert_called_once() mock_sql_using_llm.assert_called() - assert context == "" + assert context == "You have context!" assert sql == "SELECT 2;" assert isinstance(duration, float) @@ -175,7 +172,7 @@ def test_llm_command_question_verbose(mock_sql_using_llm, mock_ensure_template, r""" Invoking \llm+ returns the context and the SQL query. """ - mock_sql_using_llm.return_value = ("Verbose context, oh yeah!", "SELECT 42;") + mock_sql_using_llm.return_value = ("Verbose context, oh yeah!", "SELECT 42;", None) test_text = r"\llm+ 'Top 10 downloads by size.'" context, sql, duration = handle_llm(test_text, executor) From c24aa2c99596ff0ccac0707c6184a608edb4a0b1 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 25 May 2025 13:38:53 -0700 Subject: [PATCH 4/8] Rename CommandMode to Verbosity. --- litecli/packages/special/llm.py | 6 +++--- litecli/packages/special/main.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/litecli/packages/special/llm.py b/litecli/packages/special/llm.py index 1d46588..7ffa1ae 100644 --- a/litecli/packages/special/llm.py +++ b/litecli/packages/special/llm.py @@ -25,7 +25,7 @@ MODELS = {} from . import export -from .main import parse_special_command, CommandMode +from .main import parse_special_command, Verbosity log = logging.getLogger(__name__) @@ -220,8 +220,8 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: """ # Determine invocation mode: regular, verbose (+), or succinct (-) _, mode, arg = parse_special_command(text) - is_verbose = mode is CommandMode.VERBOSE - is_succinct = mode is CommandMode.SUCCINCT + is_verbose = mode is Verbosity.VERBOSE + is_succinct = mode is Verbosity.SUCCINCT # LLM is not installed. if llm is None: diff --git a/litecli/packages/special/main.py b/litecli/packages/special/main.py index f2c34a3..285dc2a 100644 --- a/litecli/packages/special/main.py +++ b/litecli/packages/special/main.py @@ -37,7 +37,7 @@ class CommandNotFound(Exception): pass -class CommandMode(Enum): +class Verbosity(Enum): """Mode for special command invocation: regular, verbose (+), or succinct (-).""" REGULAR = "regular" @@ -57,11 +57,11 @@ def parse_special_command(sql): # strip out any + or - modifiers to get the actual command name command = raw.strip().rstrip("+-") if is_verbose: - mode = CommandMode.VERBOSE + mode = Verbosity.VERBOSE elif is_succinct: - mode = CommandMode.SUCCINCT + mode = Verbosity.SUCCINCT else: - mode = CommandMode.REGULAR + mode = Verbosity.REGULAR return (command, mode, arg.strip()) @@ -137,7 +137,7 @@ def execute(cur, sql): if special_cmd.arg_type == NO_QUERY: return special_cmd.handler() elif special_cmd.arg_type == PARSED_QUERY: - return special_cmd.handler(cur=cur, arg=arg, verbose=(mode is CommandMode.VERBOSE)) + return special_cmd.handler(cur=cur, arg=arg, verbose=(mode is Verbosity.VERBOSE)) elif special_cmd.arg_type == RAW_QUERY: return special_cmd.handler(cur=cur, query=sql) From a38cfbf50b8e59f89f4beb77a19a0defb08c7186 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 25 May 2025 17:26:23 -0700 Subject: [PATCH 5/8] Make llm library a dependency. --- litecli/packages/special/llm.py | 34 +++++++-------------------------- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/litecli/packages/special/llm.py b/litecli/packages/special/llm.py index 7ffa1ae..103b8dd 100644 --- a/litecli/packages/special/llm.py +++ b/litecli/packages/special/llm.py @@ -2,34 +2,26 @@ import io import logging import os +import pprint import re import shlex import sys from runpy import run_module -from typing import Optional, Tuple from time import time +from typing import Optional, Tuple import click -import pprint - -try: - import llm - from llm.cli import cli - - LLM_CLI_COMMANDS = list(cli.commands.keys()) - MODELS = {x.model_id: None for x in llm.get_models()} -except ImportError: - llm = None - cli = None - LLM_CLI_COMMANDS = [] - MODELS = {} +import llm +from llm.cli import cli from . import export -from .main import parse_special_command, Verbosity +from .main import Verbosity, parse_special_command log = logging.getLogger(__name__) LLM_TEMPLATE_NAME = "litecli-llm-template" +LLM_CLI_COMMANDS = list(cli.commands.keys()) +MODELS = {x.model_id: None for x in llm.get_models()} def run_external_cmd(cmd, *args, capture_output=False, restart_cli=False, raise_exception=True) -> Tuple[int, str]: @@ -187,13 +179,6 @@ def __init__(self, results=None): """ -def initialize_llm(): - # Initialize the LLM library. - if click.confirm("This feature requires additional libraries. Install LLM library?", default=True): - click.echo("Installing LLM library. Please wait...") - run_external_cmd("pip", "install", "--quiet", "llm", restart_cli=True) - - def ensure_litecli_template(replace=False): """ Create a template called litecli with the default prompt. @@ -223,11 +208,6 @@ def handle_llm(text, cur) -> Tuple[str, Optional[str], float]: is_verbose = mode is Verbosity.VERBOSE is_succinct = mode is Verbosity.SUCCINCT - # LLM is not installed. - if llm is None: - initialize_llm() - raise FinishIteration(None) - if not arg.strip(): # No question provided. Print usage and bail. output = [(None, None, None, USAGE)] raise FinishIteration(output) diff --git a/pyproject.toml b/pyproject.toml index 8046986..e2f8eef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "sqlparse>=0.4.4", "setuptools", # Required by llm commands to install models "pip", + "llm>=0.25.0", ] [build-system] From 1efcd02c6dff8409b80cb471cf90914a8a86a3dc Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 25 May 2025 17:28:35 -0700 Subject: [PATCH 6/8] Update changelog. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3aad58..9d910d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ### Features * Replace sqlite3 library with [sqlean](https://antonz.org/sqlean/). It's a drop-in replacement for sqlite3. +* The 'llm' library is now a default dependency not installed on demand. +* The `\llm` command now has three modes. Succinct, Regular and Verbose. + + Succinct = `\llm-` - This will return just the sql query. No explanation. + Regular = `\llm` - This will return just the sql query and the explanation. + Verbose = `\llm+` - This will print the prompt sent to the LLM and the sql query and the explanation. ### Bug Fixes From db36a0489340f532f33a245ab59558ab54b22ea8 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Mon, 26 May 2025 10:30:19 -0700 Subject: [PATCH 7/8] Update tests. --- tests/conftest.py | 2 ++ tests/test_llm_special.py | 16 ---------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c6524ca..35d79b5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,10 @@ from __future__ import print_function import os + import pytest from utils import create_db, db_connection, drop_tables + import litecli.sqlexecute diff --git a/tests/test_llm_special.py b/tests/test_llm_special.py index 034592b..2e48b95 100644 --- a/tests/test_llm_special.py +++ b/tests/test_llm_special.py @@ -3,22 +3,6 @@ from litecli.packages.special.llm import handle_llm, FinishIteration, USAGE -@patch("litecli.packages.special.llm.initialize_llm") -@patch("litecli.packages.special.llm.llm", new=None) -def test_llm_command_without_install(mock_initialize_llm, executor): - """ - Test that handle_llm initializes llm when it is None and raises FinishIteration. - """ - test_text = r"\llm" - cur_mock = executor - - with pytest.raises(FinishIteration) as exc_info: - handle_llm(test_text, cur_mock) - - mock_initialize_llm.assert_called_once() - assert exc_info.value.args[0] is None - - @patch("litecli.packages.special.llm.llm") def test_llm_command_without_args(mock_llm, executor): r""" From 4722caa8f8ad4359bde62f0a06335e4ace17feac Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Mon, 26 May 2025 17:15:38 -0700 Subject: [PATCH 8/8] Remove unused variables. --- litecli/packages/completion_engine.py | 2 +- litecli/packages/special/llm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litecli/packages/completion_engine.py b/litecli/packages/completion_engine.py index adecd20..2d9a033 100644 --- a/litecli/packages/completion_engine.py +++ b/litecli/packages/completion_engine.py @@ -86,7 +86,7 @@ def suggest_type(full_text, text_before_cursor): def suggest_special(text): text = text.lstrip() - cmd, mode, arg = parse_special_command(text) + cmd, _, arg = parse_special_command(text) if cmd == text: # Trying to complete the special command itself diff --git a/litecli/packages/special/llm.py b/litecli/packages/special/llm.py index 103b8dd..c6294d5 100644 --- a/litecli/packages/special/llm.py +++ b/litecli/packages/special/llm.py @@ -292,7 +292,7 @@ def is_llm_command(command) -> bool: """ Is this an llm/ai command? """ - cmd, mode, arg = parse_special_command(command) + cmd, _, _ = parse_special_command(command) return cmd in ("\\llm", "\\ai", ".llm", ".ai")