diff --git a/site-packages/devchat/_cli/__init__.py b/site-packages/devchat/_cli/__init__.py index f186d9c..f132683 100644 --- a/site-packages/devchat/_cli/__init__.py +++ b/site-packages/devchat/_cli/__init__.py @@ -2,10 +2,12 @@ from .log import log from .prompt import prompt from .run import run from .topic import topic +from .route import route __all__ = [ 'log', 'prompt', 'run', - 'topic' + 'topic', + 'route' ] diff --git a/site-packages/devchat/_cli/main.py b/site-packages/devchat/_cli/main.py index 84aada9..f5e6007 100644 --- a/site-packages/devchat/_cli/main.py +++ b/site-packages/devchat/_cli/main.py @@ -8,6 +8,7 @@ from devchat._cli import log from devchat._cli import prompt from devchat._cli import run from devchat._cli import topic +from devchat._cli import route logger = get_logger(__name__) click.rich_click.USE_MARKDOWN = True @@ -24,3 +25,4 @@ main.add_command(prompt) main.add_command(log) main.add_command(run) main.add_command(topic) +main.add_command(route) diff --git a/site-packages/devchat/_cli/prompt.py b/site-packages/devchat/_cli/prompt.py index 7b991a1..260e529 100644 --- a/site-packages/devchat/_cli/prompt.py +++ b/site-packages/devchat/_cli/prompt.py @@ -8,6 +8,7 @@ from devchat.openai.openai_chat import OpenAIChat, OpenAIChatConfig from devchat.store import Store from devchat.utils import parse_files from devchat._cli.utils import handle_errors, init_dir, get_model_config +from devchat._cli.router import llm_prompt @click.command() @@ -28,13 +29,11 @@ from devchat._cli.utils import handle_errors, init_dir, get_model_config help='Specify the function name when the content is the output of a function.') @click.option('-ns', '--not-store', is_flag=True, default=False, required=False, help='Do not save the conversation to the store.') -@click.option('-a', '--auto', is_flag=True, default=False, required=False, - help='Answer question by function-calling.') def prompt(content: Optional[str], parent: Optional[str], reference: Optional[List[str]], instruct: Optional[List[str]], context: Optional[List[str]], model: Optional[str], config_str: Optional[str] = None, functions: Optional[str] = None, function_name: Optional[str] = None, - not_store: Optional[bool] = False, auto: Optional[bool] = False): + not_store: Optional[bool] = False): """ This command performs interactions with the specified large language model (LLM) by sending prompts and receiving responses. @@ -68,51 +67,15 @@ def prompt(content: Optional[str], parent: Optional[str], reference: Optional[Li ``` """ - repo_chat_dir, user_chat_dir = init_dir() - - with handle_errors(): - if content is None: - content = click.get_text_stream('stdin').read() - - if content == '': - return - - instruct_contents = parse_files(instruct) - context_contents = parse_files(context) - - model, config = get_model_config(repo_chat_dir, user_chat_dir, model) - - parameters_data = config.dict(exclude_unset=True) - if config_str: - config_data = json.loads(config_str) - parameters_data.update(config_data) - openai_config = OpenAIChatConfig(model=model, **parameters_data) - - chat = OpenAIChat(openai_config) - chat_store = Store(repo_chat_dir, chat) - - assistant = Assistant(chat, chat_store, config.max_input_tokens, not not_store) - - functions_data = None - if functions is not None: - with open(functions, 'r', encoding="utf-8") as f_file: - functions_data = json.load(f_file) - assistant.make_prompt(content, instruct_contents, context_contents, functions_data, - parent=parent, references=reference, - function_name=function_name) - - click.echo(assistant.prompt.formatted_header()) - command_result = run_command( - openai_config, - model, - assistant.prompt.messages, - content, - parent, - context_contents, - auto) - if command_result is not None: - sys.exit(command_result[0]) - - for response in assistant.iterate_response(): - click.echo(response, nl=False) + llm_prompt( + content, + parent, + reference, + instruct, + context, + model, + config_str, + functions, + function_name, + not_store) sys.exit(0) diff --git a/site-packages/devchat/_cli/route.py b/site-packages/devchat/_cli/route.py new file mode 100644 index 0000000..9e1e05a --- /dev/null +++ b/site-packages/devchat/_cli/route.py @@ -0,0 +1,69 @@ +import sys +from typing import List, Optional +import rich_click as click +from devchat._cli.router import llm_route + + +@click.command() +@click.argument('content', required=False) +@click.option('-p', '--parent', help='Input the parent prompt hash to continue the conversation.') +@click.option('-r', '--reference', multiple=True, + help='Input one or more specific previous prompts to include in the current prompt.') +@click.option('-i', '--instruct', multiple=True, + help='Add one or more files to the prompt as instructions.') +@click.option('-c', '--context', multiple=True, + help='Add one or more files to the prompt as a context.') +@click.option('-m', '--model', help='Specify the model to use for the prompt.') +@click.option('--config', 'config_str', + help='Specify a JSON string to overwrite the default configuration for this prompt.') +@click.option('-a', '--auto', is_flag=True, default=False, required=False, + help='Answer question by function-calling.') +def route(content: Optional[str], parent: Optional[str], reference: Optional[List[str]], + instruct: Optional[List[str]], context: Optional[List[str]], + model: Optional[str], config_str: Optional[str] = None, + auto: Optional[bool] = False): + """ + This command performs interactions with the specified large language model (LLM) + by sending prompts and receiving responses. + + Examples + -------- + + To send a multi-line message to the LLM, use the here-doc syntax: + + ```bash + devchat prompt << 'EOF' + What is the capital of France? + Can you tell me more about its history? + EOF + ``` + + Note the quotes around EOF in the first line, to prevent the shell from expanding variables. + + Configuration + ------------- + + DevChat CLI reads configuration from `~/.chat/config.yml` + (if `~/.chat` is not accessible, it will try `.chat` in your current Git or SVN root directory). + You can edit the file to modify default configuration. + + To use OpenAI's APIs, you have to set an API key by the environment variable `OPENAI_API_KEY`. + Run the following command line with your API key: + + ```bash + export OPENAI_API_KEY="sk-..." + ``` + + """ + llm_route( + content, + parent, + reference, + instruct, + context, + model, + config_str, + auto + ) + sys.exit(0) + diff --git a/site-packages/devchat/_cli/router.py b/site-packages/devchat/_cli/router.py new file mode 100644 index 0000000..3210783 --- /dev/null +++ b/site-packages/devchat/_cli/router.py @@ -0,0 +1,116 @@ +import json +import sys +from typing import List, Optional +import rich_click as click +from devchat.engine import run_command +from devchat.assistant import Assistant +from devchat.openai.openai_chat import OpenAIChat, OpenAIChatConfig +from devchat.store import Store +from devchat.utils import parse_files +from devchat._cli.utils import handle_errors, init_dir, get_model_config + + + +def before_prompt(content: Optional[str], parent: Optional[str], reference: Optional[List[str]], + instruct: Optional[List[str]], context: Optional[List[str]], + model: Optional[str], config_str: Optional[str] = None, + functions: Optional[str] = None, function_name: Optional[str] = None, + not_store: Optional[bool] = False): + repo_chat_dir, user_chat_dir = init_dir() + + if content is None: + content = click.get_text_stream('stdin').read() + + if content == '': + return + + instruct_contents = parse_files(instruct) + context_contents = parse_files(context) + + model, config = get_model_config(repo_chat_dir, user_chat_dir, model) + + parameters_data = config.dict(exclude_unset=True) + if config_str: + config_data = json.loads(config_str) + parameters_data.update(config_data) + openai_config = OpenAIChatConfig(model=model, **parameters_data) + + chat = OpenAIChat(openai_config) + chat_store = Store(repo_chat_dir, chat) + + assistant = Assistant(chat, chat_store, config.max_input_tokens, not not_store) + + functions_data = None + if functions is not None: + with open(functions, 'r', encoding="utf-8") as f_file: + functions_data = json.load(f_file) + assistant.make_prompt(content, instruct_contents, context_contents, functions_data, + parent=parent, references=reference, + function_name=function_name) + return openai_config, model, assistant, content, context_contents + + + +def llm_prompt(content: Optional[str], parent: Optional[str], reference: Optional[List[str]], + instruct: Optional[List[str]], context: Optional[List[str]], + model: Optional[str], config_str: Optional[str] = None, + functions: Optional[str] = None, function_name: Optional[str] = None, + not_store: Optional[bool] = False): + with handle_errors(): + _1, _2, assistant, _3, _4 = before_prompt( + content, parent, reference, instruct, context, model, config_str, functions, function_name, not_store + ) + + click.echo(assistant.prompt.formatted_header()) + for response in assistant.iterate_response(): + click.echo(response, nl=False) + + +def llm_commmand(content: Optional[str], parent: Optional[str], reference: Optional[List[str]], + instruct: Optional[List[str]], context: Optional[List[str]], + model: Optional[str], config_str: Optional[str] = None): + with handle_errors(): + openai_config, model, assistant, content, context_contents = before_prompt( + content, parent, reference, instruct, context, model, config_str, None, None, True + ) + + click.echo(assistant.prompt.formatted_header()) + command_result = run_command( + openai_config, + model, + assistant.prompt.messages, + content, + parent, + context_contents, + False) + if command_result is not None: + sys.exit(0) + + click.echo("run command fail.") + click.echo(command_result) + sys.exit(-1) + + +def llm_route(content: Optional[str], parent: Optional[str], reference: Optional[List[str]], + instruct: Optional[List[str]], context: Optional[List[str]], + model: Optional[str], config_str: Optional[str] = None, + auto: Optional[bool] = False): + with handle_errors(): + openai_config, model, assistant, content, context_contents = before_prompt( + content, parent, reference, instruct, context, model, config_str, None, None, True + ) + + click.echo(assistant.prompt.formatted_header()) + command_result = run_command( + openai_config, + model, + assistant.prompt.messages, + content, + parent, + context_contents, + auto) + if command_result is not None: + sys.exit(command_result[0]) + + for response in assistant.iterate_response(): + click.echo(response, nl=False) \ No newline at end of file diff --git a/site-packages/devchat/_cli/run.py b/site-packages/devchat/_cli/run.py index 9f50091..756c2a7 100644 --- a/site-packages/devchat/_cli/run.py +++ b/site-packages/devchat/_cli/run.py @@ -2,7 +2,7 @@ import json import os import shutil import sys -from typing import List +from typing import List, Optional import rich_click as click try: from git import Repo, GitCommandError @@ -12,6 +12,7 @@ from devchat._cli.utils import init_dir, handle_errors, valid_git_repo, clone_gi from devchat._cli.utils import download_and_extract_workflow from devchat.engine import Namespace, CommandParser, RecursivePrompter from devchat.utils import get_logger +from devchat._cli.router import llm_commmand logger = get_logger(__name__) @@ -24,7 +25,21 @@ logger = get_logger(__name__) help='List commands recursively.') @click.option('--update-sys', 'update_sys_flag', is_flag=True, default=False, help='Pull the `sys` command directory from the DevChat repository.') -def run(command: str, list_flag: bool, recursive_flag: bool, update_sys_flag: bool): +@click.option('-p', '--parent', help='Input the parent prompt hash to continue the conversation.') +@click.option('-r', '--reference', multiple=True, + help='Input one or more specific previous prompts to include in the current prompt.') +@click.option('-i', '--instruct', multiple=True, + help='Add one or more files to the prompt as instructions.') +@click.option('-c', '--context', multiple=True, + help='Add one or more files to the prompt as a context.') +@click.option('-m', '--model', help='Specify the model to use for the prompt.') +@click.option('--config', 'config_str', + help='Specify a JSON string to overwrite the default configuration for this prompt.') +def run(command: str, list_flag: bool, recursive_flag: bool, update_sys_flag: bool, + parent: Optional[str], reference: Optional[List[str]], + instruct: Optional[List[str]], context: Optional[List[str]], + model: Optional[str], config_str: Optional[str] = None, + auto: Optional[bool] = False): """ Operate the workflow engine of DevChat. """ @@ -43,13 +58,12 @@ def run(command: str, list_flag: bool, recursive_flag: bool, update_sys_flag: bo if update_sys_flag: sys_dir = os.path.join(workflows_dir, 'sys') git_urls = [ - 'https://gitlab.com/devchat-ai/workflows.git', - 'https://gitee.com/devchat-ai/workflows.git', - 'https://github.com/devchat-ai/workflows.git' + 'https://github.com/devchat-ai/workflows.git', + 'https://gitlab.com/devchat-ai/workflows.git' ] zip_urls = [ - 'https://gitlab.com/devchat-ai/workflows/-/archive/main/workflows-main.zip', - 'https://codeload.github.com/devchat-ai/workflows/zip/refs/heads/main' + 'https://codeload.github.com/devchat-ai/workflows/zip/refs/heads/main', + 'https://gitlab.com/devchat-ai/workflows/-/archive/main/workflows-main.zip' ] _clone_or_pull_git_repo(sys_dir, git_urls, zip_urls) return @@ -69,15 +83,15 @@ def run(command: str, list_flag: bool, recursive_flag: bool, update_sys_flag: bo return if command: - cmd = commander.parse(command) - if not cmd: - click.echo(f"Error: Failed to find command: {command}", err=True) - sys.exit(1) - if not cmd.steps: - prompter = RecursivePrompter(namespace) - click.echo(prompter.run(command)) - else: - click.echo(json.dumps(cmd.dict())) + llm_commmand( + command, + parent, + reference, + instruct, + context, + model, + config_str + ) return