From 02a56ed0b9fb7037360588c20210c7ce290a57c8 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Fri, 8 Nov 2024 09:19:13 -0500 Subject: [PATCH] Add .gitignore and pyproject.toml files --- .gitignore | 10 +++ README.md | 0 pyproject.toml | 19 +++++ simplechat/__init__.py | 1 + simplechat/cli.py | 159 +++++++++++++++++++++++++++++++++++++++++ simplechat/db.py | 41 +++++++++++ simplechat/settings.py | 43 +++++++++++ 7 files changed, 273 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 simplechat/__init__.py create mode 100644 simplechat/cli.py create mode 100644 simplechat/db.py create mode 100644 simplechat/settings.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5410881 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "simplechat" +version = "0.1.0" +description = "A chat interface for AI models using Simplemind." +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "appdirs", + "sqlalchemy", + "records", + "docopt", + "rich", + "prompt_toolkit", + "pydantic", + "xerox" +] + +[project.scripts] +simplechat = "simplechat.cli:main" diff --git a/simplechat/__init__.py b/simplechat/__init__.py new file mode 100644 index 0000000..0a53f3c --- /dev/null +++ b/simplechat/__init__.py @@ -0,0 +1 @@ +# from .db import db diff --git a/simplechat/cli.py b/simplechat/cli.py new file mode 100644 index 0000000..cdd1179 --- /dev/null +++ b/simplechat/cli.py @@ -0,0 +1,159 @@ +import docopt +import re + +import xerox +import simplemind as sm +from rich.console import Console +from rich.panel import Panel + +from .db import Database +from .settings import get_db_path + +AVAILABLE_PROVIDERS = ["xai", "openai", "anthropic", "ollama"] +AVAILABLE_COMMANDS = ["/copy", "/paste", "/help", "/exit", "/clear", "/invoke"] + +__doc__ = """Simplechat CLI + +Usage: + simplechat [--provider=] [--model=] + simplechat (-h | --help) + +Options: + -h --help Show this screen. + --provider= LLM provider to use (openai/anthropic/xai/ollama) + --model= Specific model to use (e.g. o1-preview) +""" + + +class Simplechat: + def __init__(self): + self.llm_model = None + self.llm_provider = None + + # Prepare the database path. + self.db_path = get_db_path() + self.db_url = f"sqlite:///{self.db_path}" + + # Initialize the database. + self.db = Database(self.db_url) + self.sm = sm.Session() + + def __str__(self): + return f"" + + def __repr__(self): + return f"" + + def set_llm(self, llm_provider, llm_model): + self.llm_provider = llm_provider + self.llm_model = llm_model + + self.sm = sm.Session(llm_provider=llm_provider, llm_model=llm_model) + + def set_llm_provider(self, llm_provider): + if llm_provider not in AVAILABLE_PROVIDERS: + raise ValueError(f"Unsupported provider: {llm_provider!r}") + + self.llm_provider = llm_provider + + def repl(self): + """Start an interactive REPL session.""" + from prompt_toolkit import PromptSession + from prompt_toolkit.styles import Style + from rich.console import Console + from prompt_toolkit.completion import WordCompleter + + command_completer = WordCompleter( + AVAILABLE_COMMANDS, pattern=re.compile(r"/\w*") + ) + + console = Console() + style = Style.from_dict( + { + "prompt": "#00aa00 bold", + } + ) + + session = PromptSession( + style=style, message=[("class:prompt", ">>> ")], completer=command_completer + ) + + console.print("[bold green]Welcome to Simplechat![/bold green]") + # console.print(f"Using provider: {self.llm_provider or 'default'}") + # console.print(f"Using model: {self.llm_model or 'default'}") + console.print("Type '/help' for available commands\n") + + while True: + try: + # Get the user input with autocompletion + user_input = session.prompt().strip() + + # Handle commands + if user_input.startswith("/"): + # Exit command. + if user_input.lower() in ("/exit", "/quit", "/q"): + break + + # Help command. + elif user_input == "/help": + console.print("\nAvailable commands:") + + for cmd in AVAILABLE_COMMANDS: + console.print(f" {cmd}") + + console.print() + continue + + # Copy to clipboard. + elif user_input == "/copy": + console.print( + "[bold green]Copying to clipboard...[/bold green]" + ) + xerox.copy(user_input) + + # Paste from clipboard. + elif user_input == "/paste": + console.print( + "[bold green]Pasting from clipboard...[/bold green]" + ) + clipboard_content = xerox.paste() + if clipboard_content: + # Print the pasted content + console.print() # Add blank line + console.print( + Panel.fit( + clipboard_content, + title="[bold]Pasted Content[/bold]", + border_style="blue", + ) + ) + continue + + # Send the input to the LLM and get response + response = self.sm.send(user_input) + console.print(f"\n[bold blue]Assistant:[/bold blue] {response}\n") + + except KeyboardInterrupt: + exit(1) + except EOFError: + break + except Exception as e: + console.print(f"[bold red]Error:[/bold red] {str(e)}\n") + + console.print("\nGoodbye!") + + +def main(): + args = docopt.docopt(__doc__) + + simplechat = Simplechat() + + # Set the LLM provider and model. + simplechat.set_llm(llm_provider=args["--provider"], llm_model=args["--model"]) + + # Start the conversation. + simplechat.repl() + + +if __name__ == "__main__": + main() diff --git a/simplechat/db.py b/simplechat/db.py new file mode 100644 index 0000000..3f8c939 --- /dev/null +++ b/simplechat/db.py @@ -0,0 +1,41 @@ +from records import Database as RecordsDatabase + + +class Database: + def __init__(self, db_path="sqlite:///simplechat.db", *, migrate=True): + # Initialize the database. + self.db = RecordsDatabase(db_path) + + if migrate: + # Perform migration. + self.migrate() + + def migrate(self): + """Creates the tables.""" + scheme_1 = """ + CREATE TABLE IF NOT EXISTS memory ( + entity TEXT, + source TEXT, + last_mentioned TIMESTAMP, + mention_count INTEGER DEFAULT 1, + PRIMARY KEY (entity, source) + ) + """ + + scheme_2 = """ + CREATE TABLE IF NOT EXISTS essence_markers ( + marker_type TEXT, + marker_text TEXT, + timestamp TIMESTAMP, + PRIMARY KEY (marker_type, marker_text) + ) + """ + + schemes = [scheme_1, scheme_2] + + for scheme in schemes: + self.query(scheme) + + @property + def query(self): + return self.db.query diff --git a/simplechat/settings.py b/simplechat/settings.py new file mode 100644 index 0000000..7ffe054 --- /dev/null +++ b/simplechat/settings.py @@ -0,0 +1,43 @@ +import os +import pathlib +from appdirs import AppDirs + +_NAME = "simplechat" +_AUTHOR = "simplechat" +_DEFAULT_DB_NAME = "simplechat.db" + + +def get_app_dir(): + """Returns the AppDirs object for the application.""" + + return AppDirs(_NAME, _AUTHOR) + + +def mkdir_p(path): + """Create a directory if it doesn't exist.""" + + if not os.path.exists(path): + return os.makedirs(path) + + +def get_config_dir(): + """Returns the full path to the configuration directory.""" + + appdir = get_app_dir() + config_dir = appdir.user_config_dir + + # Make the directory, if it doesn't exist. + mkdir_p(config_dir) + + return pathlib.Path(appdir.user_config_dir) + + +def get_db_path(): + """Returns the full path to the database file.""" + + config_dir = get_config_dir() + return config_dir / _DEFAULT_DB_NAME + + +if __name__ == "__main__": + print(get_db_path())