├── main.py ├── tests ├── __init__.py ├── data_types_test.py ├── execute_python_test.py └── deepseek_test.py ├── .python-version ├── modules ├── __init__.py ├── data_types.py ├── execute_python.py ├── ollama.py ├── assistant_config.py ├── utils.py ├── base_assistant.py ├── deepseek.py └── typer_agent.py ├── ai_docs └── ai_docs.md ├── .env.sample ├── images └── ada-deepseek-v3.png ├── ada.sh ├── deepseek-architect.sh ├── o1-architect-deepseek-editor.sh ├── sonnet-architect.sh ├── assistant_config.yml ├── pyproject.toml ├── prompts ├── concise-assistant-response.xml └── typer-commands.xml ├── check.py ├── main_base_assistant.py ├── README.md ├── .gitignore ├── main_typer_assistant.py ├── scratchpad.md ├── commands ├── template_empty.py └── template.py └── .template.aider.conf.yml /main.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11 -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ai_docs/ai_docs.md: -------------------------------------------------------------------------------- 1 | Place docs for your AI here. -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | DEEPSEEK_API_KEY= 2 | ELEVEN_API_KEY= 3 | -------------------------------------------------------------------------------- /images/ada-deepseek-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disler/always-on-ai-assistant/HEAD/images/ada-deepseek-v3.png -------------------------------------------------------------------------------- /modules/data_types.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class MockDataType(BaseModel): 5 | id: str 6 | name: str 7 | -------------------------------------------------------------------------------- /ada.sh: -------------------------------------------------------------------------------- 1 | uv run python main_typer_assistant.py awaken \ 2 | --typer-file commands/template.py \ 3 | --scratchpad scratchpad.md \ 4 | --mode execute -------------------------------------------------------------------------------- /deepseek-architect.sh: -------------------------------------------------------------------------------- 1 | aider --model deepseek/deepseek-chat --architect --editor-model deepseek/deepseek-chat --yes-always --no-detect-urls --load .aider -------------------------------------------------------------------------------- /o1-architect-deepseek-editor.sh: -------------------------------------------------------------------------------- 1 | aider --model o1-2024-12-17 --architect --editor-model deepseek/deepseek-chat --yes-always --no-detect-urls --load .aider -------------------------------------------------------------------------------- /sonnet-architect.sh: -------------------------------------------------------------------------------- 1 | aider --model claude-3-5-sonnet-20241022 --architect --editor-model claude-3-5-sonnet-20241022 --yes-always --no-detect-urls --load .aider -------------------------------------------------------------------------------- /tests/data_types_test.py: -------------------------------------------------------------------------------- 1 | from modules.data_types import MockDataType 2 | 3 | 4 | def test_mock_data_type(): 5 | # Arrange 6 | mock_id = "test-id" 7 | mock_name = "Test Name" 8 | 9 | # Act 10 | mock_data = MockDataType(id=mock_id, name=mock_name) 11 | 12 | # Assert 13 | assert mock_data.id == mock_id 14 | assert mock_data.name == mock_name 15 | -------------------------------------------------------------------------------- /assistant_config.yml: -------------------------------------------------------------------------------- 1 | typer_assistant: 2 | assistant_name: Ada 3 | human_companion_name: Dan 4 | ears: realtime-stt 5 | brain: deepseek-v3 # only deepseek-v3 6 | voice: elevenlabs # local, elevenlabs 7 | elevenlabs_voice: WejK3H1m7MI9CHnIjW9K 8 | base_assistant: 9 | assistant_name: Ada 10 | human_companion_name: Dan 11 | ears: realtime-stt 12 | brain: ollama:phi4 # deepseek-v3, ollama:phi4, ollama: 13 | voice: local # local, elevenlabs 14 | elevenlabs_voice: WejK3H1m7MI9CHnIjW9K -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "paic-python-starter" 3 | version = "0.1.1" 4 | description = "PAIC Python Starter" 5 | readme = "README.md" 6 | requires-python = ">=3.10,<3.12" 7 | dependencies = [ 8 | "anthropic>=0.39.0", 9 | "google-generativeai>=0.8.3", 10 | "llm>=0.18", 11 | "llm-claude-3>=0.9", 12 | "llm-gemini>=0.4.2", 13 | "ollama>=0.4.1", 14 | "openai[realtime]>=1.59.0", 15 | "pydantic>=2.10.2", 16 | "pytest>=8.3.3", 17 | "python-dotenv>=1.0.1", 18 | "pyyaml>=6.0.2", 19 | "typer>=0.13.1", 20 | "rich>=13.9.4", 21 | "realtimestt>=0.3.93", 22 | "dpath>=2.2.0", 23 | "elevenlabs>=1.50.3", 24 | "realtimetts[system]==0.4.40", 25 | "pyttsx3>=2.98", 26 | ] 27 | -------------------------------------------------------------------------------- /modules/execute_python.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import shlex 3 | 4 | 5 | def execute_uv_python(command: str, file_path: str) -> str: 6 | """Execute the tests and return the output as a string.""" 7 | complete_command = f"uv run python {file_path} {command}" 8 | 9 | return execute(complete_command) 10 | 11 | 12 | def execute(command: str) -> str: 13 | """Execute shell code and return the output as a string.""" 14 | try: 15 | # Use shell=True to properly handle shell operators like && 16 | result = subprocess.run( 17 | command, 18 | shell=True, 19 | capture_output=True, 20 | text=True, 21 | ) 22 | return result.stdout + result.stderr 23 | except subprocess.SubprocessError as e: 24 | return str(e) 25 | -------------------------------------------------------------------------------- /prompts/concise-assistant-response.xml: -------------------------------------------------------------------------------- 1 | 2 | You are a friendly, ultra helpful, attentive, concise AI assistant named '{personal_ai_assistant_name}'. 3 | You're communicating with '{human_companion_name}' and providing feedback based on your latest-action. 4 | 5 | 6 | 7 | You work in a close-knit, highly efficient, one on one team with '{human_companion_name}'. 8 | You work with your human companion '{human_companion_name}' to build, collaborate, and connect. 9 | We both like short, concise, conversational interactions. Exclude meta-data, slashes, file paths,markdown, dashes, asterisks, etc. 10 | You're providing a concise piece of feedback based on your latest-action. 11 | We like to keep things neutral and forward focused. 12 | 13 | 14 | 15 | {{latest_action}} 16 | 17 | 18 | 19 | {{human_companion_name}} 20 | 21 | -------------------------------------------------------------------------------- /modules/ollama.py: -------------------------------------------------------------------------------- 1 | from ollama import chat 2 | from typing import List, Dict 3 | 4 | 5 | def conversational_prompt( 6 | messages: List[Dict[str, str]], 7 | system_prompt: str = "You are a helpful conversational assistant. Respond in a short, concise, friendly manner.", 8 | model: str = "phi4", 9 | ) -> str: 10 | """ 11 | Send a conversational prompt to Ollama with message history. 12 | 13 | Args: 14 | messages: List of message dicts with 'role' and 'content' keys 15 | system_prompt: Optional system prompt to set context 16 | model: The model to use, defaults to llama2 17 | 18 | Returns: 19 | str: The model's response 20 | """ 21 | try: 22 | # Add system prompt as first message 23 | full_messages = [{"role": "system", "content": system_prompt}, *messages] 24 | 25 | response = chat( 26 | model=model, 27 | messages=full_messages, 28 | ) 29 | return response.message.content 30 | 31 | except Exception as e: 32 | raise Exception(f"Error in conversational prompt: {str(e)}") 33 | -------------------------------------------------------------------------------- /check.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import ctranslate2 3 | 4 | # Check if CUDA is available 5 | print(f"CUDA available: {torch.cuda.is_available()}") 6 | 7 | # Get the number of available GPUs 8 | print(f"Number of GPUs: {torch.cuda.device_count()}") 9 | 10 | # Get the name of the current GPU 11 | if torch.cuda.is_available(): 12 | print(f"Current GPU: {torch.cuda.get_device_name(0)}") 13 | 14 | # Create a sample tensor and move it to GPU 15 | x = torch.rand(5, 3) 16 | if torch.cuda.is_available(): 17 | x = x.cuda() 18 | print(f"Tensor is on GPU: {x.is_cuda}") 19 | else: 20 | print("Tensor is on CPU") 21 | 22 | # Check the device of the tensor 23 | print(f"Device: {x.device}") 24 | 25 | # Perform a simple operation to test GPU usage 26 | if torch.cuda.is_available(): 27 | start = torch.cuda.Event(enable_timing=True) 28 | end = torch.cuda.Event(enable_timing=True) 29 | 30 | start.record() 31 | result = torch.matmul(x, x.t()) 32 | end.record() 33 | 34 | # Waits for everything to finish running 35 | torch.cuda.synchronize() 36 | 37 | print(f"GPU computation time: {start.elapsed_time(end)} milliseconds") 38 | 39 | # Check CTranslate2 device info 40 | # print( 41 | # f"CTranslate2 GPU supported types: {ctranslate2.get_supported_compute_types('cuda')}" 42 | # ) 43 | -------------------------------------------------------------------------------- /modules/assistant_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | from dpath import util as dpath_util 4 | 5 | DEFAULT_CONFIG_PATH = "assistant_config.yml" 6 | 7 | 8 | def get_config(dot_path_key: str, config_path: str = DEFAULT_CONFIG_PATH) -> str: 9 | """ 10 | Load a field from the YAML config file using dot notation path. 11 | 12 | Args: 13 | dot_path_key: The key path to look up in the config (e.g. 'parent.child.key') 14 | config_path: Path to the YAML config file, defaults to assistant_config.yml 15 | 16 | Returns: 17 | str: The value for the requested key path 18 | 19 | Raises: 20 | FileNotFoundError: If config file doesn't exist 21 | KeyError: If ke{y path not found in config 22 | """ 23 | # Get absolute path from current working directory 24 | abs_config_path = os.path.join(os.getcwd(), config_path) 25 | 26 | if not os.path.exists(abs_config_path): 27 | raise FileNotFoundError(f"Config file not found at {abs_config_path}") 28 | 29 | with open(abs_config_path) as f: 30 | config = yaml.safe_load(f) 31 | 32 | try: 33 | return dpath_util.get(config, dot_path_key, separator=".") 34 | except KeyError: 35 | raise KeyError(f"Key path '{dot_path_key}' not found in config") 36 | 37 | 38 | def get_config_file(config_path: str = DEFAULT_CONFIG_PATH) -> str: 39 | """ 40 | Get the config file as a string. 41 | """ 42 | with open(config_path, "r") as f: 43 | return f.read() 44 | -------------------------------------------------------------------------------- /tests/execute_python_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from modules.execute_python import execute, execute_uv_python 3 | import tempfile 4 | import os 5 | 6 | 7 | def test_execute_basic_command(): 8 | """Test basic command execution""" 9 | result = execute("echo Hello World") 10 | assert "Hello World" in result 11 | assert len(result.strip()) > 0 12 | 13 | 14 | def test_execute_error_command(): 15 | """Test command execution with error""" 16 | result = execute("false") 17 | assert not result # Should have some error output 18 | 19 | 20 | def test_execute_uv_python_with_file(tmp_path): 21 | """Test uv python execution with a temporary python file""" 22 | # Create temporary python file 23 | test_file = tmp_path / "test_script.py" 24 | test_file.write_text("print('Hello from temp file')") 25 | 26 | # Execute the file 27 | result = execute_uv_python("", str(test_file)) 28 | assert "Hello from temp file" in result 29 | 30 | 31 | def test_execute_uv_python_with_args(tmp_path): 32 | """Test uv python execution with arguments""" 33 | # Create temporary python file 34 | test_file = tmp_path / "test_script.py" 35 | test_file.write_text( 36 | """ 37 | import sys 38 | print(f'Arguments: {sys.argv[1:]}') 39 | """ 40 | ) 41 | 42 | # Execute with arguments 43 | result = execute_uv_python("arg1 arg2", str(test_file)) 44 | assert "Arguments: ['arg1', 'arg2']" in result 45 | 46 | 47 | def test_execute_uv_python_error(tmp_path): 48 | """Test uv python execution with error""" 49 | # Create temporary python file with error 50 | test_file = tmp_path / "test_script.py" 51 | test_file.write_text("raise ValueError('Test error')") 52 | 53 | # Execute and check for error 54 | result = execute_uv_python("", str(test_file)) 55 | assert "ValueError" in result 56 | assert "Test error" in result 57 | -------------------------------------------------------------------------------- /prompts/typer-commands.xml: -------------------------------------------------------------------------------- 1 | 2 | Given the available python typer-commands, the context-files,the scratch-pad and the natural-language-request produce the correct typer CLI command. 3 | 4 | 5 | 6 | We only want the CLI command as output. No explanations or additional text. The command will be immediately executed. 7 | Focus on generating one or more python typer-based commands based on the natural-language-request. 8 | All commands must be fully valid typer commands from the typer-commands file with arguments and flags infered based on the natural-language-request. 9 | Be sure to use - for the function names instead of _ (example: 'hello-world' instead of 'hello_world') 10 | Don't use = for arguments, use spaces instead (example: 'hello-world --name John' instead of 'hello-world --name=John') 11 | If multiple commands are requested chain them together with '&&' so they run back to back in the terminal only if the previous command succeeds. 12 | If asked generated multiple commands be sure to fire it with 'uv run python [typer-file]' instead of 'python'. You'll see the typer-file name in the prompt. 13 | If the natural-language-request does not ask for a command (or is something nonsense) that is not in the typer-commands file, respond with an empty string. 14 | If relevant to the natural-language-request, use the context-files to aid your decision making. 15 | If relevant to the natural-language-request, use the scratch-pad to aid your decision making. You can expect useful information in the scratch-pad especially if explicitly asked referenced in the natural-language-request. 16 | 17 | 18 | 19 | {{typer-commands}} 20 | 21 | 22 | 23 | {{context_files}} 24 | 25 | 26 | 27 | {{scratch_pad}} 28 | 29 | 30 | 31 | {{natural_language_request}} 32 | 33 | -------------------------------------------------------------------------------- /main_base_assistant.py: -------------------------------------------------------------------------------- 1 | from RealtimeSTT import AudioToTextRecorder 2 | from typing import List 3 | from modules.assistant_config import get_config 4 | from modules.base_assistant import PlainAssistant 5 | from modules.utils import create_session_logger_id, setup_logging 6 | import typer 7 | import logging 8 | 9 | app = typer.Typer() 10 | 11 | 12 | @app.command() 13 | def ping(): 14 | print("pong") 15 | 16 | 17 | @app.command() 18 | def chat(): 19 | """Start a chat session with the plain assistant using speech input""" 20 | # Create session and logging 21 | session_id = create_session_logger_id() 22 | logger = setup_logging(session_id) 23 | logger.info(f"🚀 Starting chat session {session_id}") 24 | 25 | # Create assistant 26 | assistant = PlainAssistant(logger, session_id) 27 | 28 | # Configure STT recorder 29 | recorder = AudioToTextRecorder( 30 | spinner=True, 31 | model="tiny.en", 32 | language="en", 33 | print_transcription_time=True, 34 | ) 35 | 36 | def process_text(text): 37 | """Process user speech input""" 38 | try: 39 | 40 | assistant_name = get_config("base_assistant.assistant_name") 41 | if assistant_name.lower() not in text.lower(): 42 | logger.info(f"🤖 Not {assistant_name} - ignoring") 43 | return 44 | 45 | # Check for exit commands 46 | if text.lower() in ["exit", "quit"]: 47 | logger.info("👋 Exiting chat session") 48 | return False 49 | 50 | # Process input and get response 51 | recorder.stop() 52 | response = assistant.process_text(text) 53 | logger.info(f"🤖 Response: {response}") 54 | recorder.start() 55 | 56 | return True 57 | 58 | except Exception as e: 59 | logger.error(f"❌ Error occurred: {str(e)}") 60 | raise 61 | 62 | try: 63 | print("🎤 Speak now... (say 'exit' or 'quit' to end)") 64 | while True: 65 | recorder.text(process_text) 66 | 67 | except KeyboardInterrupt: 68 | logger.info("👋 Session ended by user") 69 | raise KeyboardInterrupt 70 | except Exception as e: 71 | logger.error(f"❌ Error occurred: {str(e)}") 72 | raise 73 | 74 | 75 | if __name__ == "__main__": 76 | app() 77 | -------------------------------------------------------------------------------- /tests/deepseek_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from modules.deepseek import ( 3 | prompt, 4 | fill_in_the_middle_prompt, 5 | json_prompt, 6 | prefix_prompt, 7 | prefix_then_stop_prompt, 8 | ) 9 | 10 | 11 | def test_prompt(): 12 | """Test basic prompt functionality""" 13 | response = prompt("Hello, how are you?") 14 | assert isinstance(response, str) 15 | assert len(response) > 0 16 | 17 | 18 | def test_fill_in_the_middle_prompt(): 19 | """Test fill-in-the-middle prompt with an SQL query example""" 20 | response = fill_in_the_middle_prompt( 21 | prompt="""SELECT 22 | customer_id, 23 | first_name, 24 | last_name, 25 | """, 26 | suffix=""" 27 | ORDER BY last_name ASC; 28 | """, 29 | ) 30 | 31 | assert isinstance(response, str) 32 | assert len(response) > 10 # Verify meaningful content 33 | assert "FROM" in response # Verify suffix is not included 34 | 35 | # Verify SQL syntax and common patterns 36 | assert any( 37 | word in response for word in ["email", "phone", "address", "city", "state"] 38 | ) # Common customer columns 39 | assert "," in response # Verify proper column separation 40 | assert "\n" in response # Verify formatting 41 | 42 | 43 | def test_json_prompt(): 44 | """Test JSON response format""" 45 | response = json_prompt("Return a JSON object with a 'message' key") 46 | assert isinstance(response, dict) 47 | assert "message" in response 48 | assert isinstance(response["message"], str) 49 | 50 | 51 | def test_prefix_prompt(): 52 | """Test prefix-constrained prompt""" 53 | response = prefix_prompt( 54 | prompt="Complete this sentence: The best programming language is", 55 | prefix="The best programming language is Python", 56 | ) 57 | print("response", response) 58 | assert isinstance(response, str) 59 | # Make assertion more flexible to handle different responses 60 | assert "Python" in response # Just verify Python is mentioned 61 | assert len(response) > len( 62 | "The best programming language is Python" 63 | ) # Verify response continues 64 | 65 | 66 | def test_prefix_suffix_prompt(): 67 | """Test prefix and suffix constrained prompt""" 68 | response = prefix_then_stop_prompt( 69 | prompt="Generate python code in a markdown code block that completes this function: def csvs_to_sqlite_tables(csvs: List[str], sqlite_db: str)", 70 | prefix="```python", 71 | suffix="```", 72 | ) 73 | print("response", response) 74 | assert isinstance(response, str) 75 | assert "def csvs_to_sqlite_tables" in response 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # "Always-On" Deepseek AI Assistant 2 | > A pattern for an always on AI Assistant powered by Deepseek-V3, RealtimeSTT, and Typer for engineering 3 | > 4 | > Checkout [the demo](https://youtu.be/zoBwIi4ZiTA) where we walk through using this always-on-ai-assistant. 5 | 6 | ![ada-deepseek-v3.png](./images/ada-deepseek-v3.png) 7 | 8 | ## Setup 9 | - `cp .env.sample .env` 10 | - Update with your keys `DEEPSEEK_API_KEY` and `ELEVEN_API_KEY` 11 | - `uv sync` 12 | - (optional) install python 3.11 (`uv python install 3.11`) 13 | 14 | 15 | ## Commands 16 | 17 | ### Base Assistant Chat Interface 18 | > See `main_base_assistant.py` for more details. 19 | Start a conversational chat session with the base assistant: 20 | 21 | ```bash 22 | uv run python main_base_assistant.py chat 23 | ``` 24 | 25 | ### Typer Assistant Conversational Commands 26 | > See `main_typer_assistant.py`, `modules/typer_agent.py`, and `commands/template.py` for more details. 27 | 28 | - `--typer-file`: file containing typer commands 29 | - `--scratchpad`: active memory for you and your assistant 30 | - `--mode`: determines what the assistant does with the command: ('default', 'execute', 'execute-no-scratch'). 31 | 32 | 1. Awaken the assistant 33 | ```bash 34 | uv run python main_typer_assistant.py awaken --typer-file commands/template.py --scratchpad scratchpad.md --mode execute 35 | ``` 36 | 37 | 2. Speak to the assistant 38 | Try this: 39 | "Hello! Ada, ping the server wait for a response" (be sure to pronounce 'ada' clearly) 40 | 41 | 3. See the command in the scratchpad 42 | Open `scratchpad.md` to see the command that was generated. 43 | 44 | ## Assistant Architecture 45 | > See `assistant_config.yml` for more details. 46 | 47 | ### Typer Assistant 48 | > See `assistant_config.yml` for more details. 49 | - 🧠 Brain: `Deepseek V3` 50 | - 📝 Job (Prompt(s)): `prompts/typer-commands.xml` 51 | - 💻 Active Memory (Dynamic Variables): `scratchpad.txt` 52 | - 👂 Ears (STT): `RealtimeSTT` 53 | - 🎤 Mouth (TTS): `ElevenLabs` 54 | 55 | ### Base Assistant 56 | > See `assistant_config.yml` for more details. 57 | - 🧠 Brain: `ollama:phi4` 58 | - 📝 Job (Prompt(s)): `None` 59 | - 💻 Active Memory (Dynamic Variables): `none` 60 | - 👂 Ears (STT): `RealtimeSTT` 61 | - 🎤 Mouth (TTS): `local` 62 | 63 | 64 | ## Resources 65 | - LOCAL SPEECH TO TEXT: https://github.com/KoljaB/RealtimeSTT 66 | - faster whisper (support for RealtimeSTT) https://github.com/SYSTRAN/faster-whisper 67 | - whisper https://github.com/openai/whisper 68 | - examples https://github.com/KoljaB/RealtimeSTT/blob/master/tests/realtimestt_speechendpoint_binary_classified.py 69 | - elevenlabs voice models: https://elevenlabs.io/docs/developer-guides/models#older-models -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | input/ 3 | 4 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 5 | 6 | # Logs 7 | 8 | logs 9 | _.log 10 | npm-debug.log_ 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | .pnpm-debug.log* 15 | 16 | # Caches 17 | 18 | .cache 19 | 20 | # Diagnostic reports (https://nodejs.org/api/report.html) 21 | 22 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 23 | 24 | # Runtime data 25 | 26 | pids 27 | _.pid 28 | _.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | 37 | coverage 38 | *.lcov 39 | 40 | # nyc test coverage 41 | 42 | .nyc_output 43 | 44 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 45 | 46 | .grunt 47 | 48 | # Bower dependency directory (https://bower.io/) 49 | 50 | bower_components 51 | 52 | # node-waf configuration 53 | 54 | .lock-wscript 55 | 56 | # Compiled binary addons (https://nodejs.org/api/addons.html) 57 | 58 | build/Release 59 | 60 | # Dependency directories 61 | 62 | node_modules/ 63 | jspm_packages/ 64 | 65 | # Snowpack dependency directory (https://snowpack.dev/) 66 | 67 | web_modules/ 68 | 69 | # TypeScript cache 70 | 71 | *.tsbuildinfo 72 | 73 | # Optional npm cache directory 74 | 75 | .npm 76 | 77 | # Optional eslint cache 78 | 79 | .eslintcache 80 | 81 | # Optional stylelint cache 82 | 83 | .stylelintcache 84 | 85 | # Microbundle cache 86 | 87 | .rpt2_cache/ 88 | .rts2_cache_cjs/ 89 | .rts2_cache_es/ 90 | .rts2_cache_umd/ 91 | 92 | # Optional REPL history 93 | 94 | .node_repl_history 95 | 96 | # Output of 'npm pack' 97 | 98 | *.tgz 99 | 100 | # Yarn Integrity file 101 | 102 | .yarn-integrity 103 | 104 | # dotenv environment variable files 105 | 106 | .env 107 | .env.development.local 108 | .env.test.local 109 | .env.production.local 110 | .env.local 111 | 112 | # parcel-bundler cache (https://parceljs.org/) 113 | 114 | .parcel-cache 115 | 116 | # Next.js build output 117 | 118 | .next 119 | out 120 | 121 | # Nuxt.js build / generate output 122 | 123 | .nuxt 124 | dist 125 | 126 | # Gatsby files 127 | 128 | # Comment in the public line in if your project uses Gatsby and not Next.js 129 | 130 | # https://nextjs.org/blog/next-9-1#public-directory-support 131 | 132 | # public 133 | 134 | # vuepress build output 135 | 136 | .vuepress/dist 137 | 138 | # vuepress v2.x temp and cache directory 139 | 140 | .temp 141 | 142 | # Docusaurus cache and generated files 143 | 144 | .docusaurus 145 | 146 | # Serverless directories 147 | 148 | .serverless/ 149 | 150 | # FuseBox cache 151 | 152 | .fusebox/ 153 | 154 | # DynamoDB Local files 155 | 156 | .dynamodb/ 157 | 158 | # TernJS port file 159 | 160 | .tern-port 161 | 162 | # Stores VSCode versions used for testing VSCode extensions 163 | 164 | .vscode-test 165 | 166 | # yarn v2 167 | 168 | .yarn/cache 169 | .yarn/unplugged 170 | .yarn/build-state.yml 171 | .yarn/install-state.gz 172 | .pnp.* 173 | 174 | # IntelliJ based IDEs 175 | .idea 176 | 177 | # Finder (MacOS) folder config 178 | .DS_Store 179 | .aider* 180 | 181 | __pycache__/ 182 | 183 | .venv/ 184 | 185 | apps/marimo-prompt-library/prompt_executions 186 | 187 | prompt_executions/ 188 | 189 | apps/ada/personalization.json 190 | apps/ada/runtime_time_table.jsonl 191 | 192 | session_dir/ 193 | 194 | specs/ 195 | adw/ 196 | 197 | realtimesst.log 198 | 199 | *.db 200 | 201 | report.json -------------------------------------------------------------------------------- /modules/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os 4 | from typing import Union, Dict, List 5 | import uuid 6 | 7 | OUTPUT_DIR = "output" 8 | 9 | 10 | def build_file_path(name: str): 11 | session_dir = f"{OUTPUT_DIR}" 12 | os.makedirs(session_dir, exist_ok=True) 13 | return os.path.join(session_dir, f"{name}") 14 | 15 | 16 | def build_file_name_session(name: str, session_id: str): 17 | session_dir = f"{OUTPUT_DIR}/{session_id}" 18 | os.makedirs(session_dir, exist_ok=True) 19 | return os.path.join(session_dir, f"{name}") 20 | 21 | 22 | def to_json_file_pretty(name: str, content: Union[Dict, List]): 23 | def default_serializer(obj): 24 | if hasattr(obj, "model_dump"): 25 | return obj.model_dump() 26 | raise TypeError( 27 | f"Object of type {obj.__class__.__name__} is not JSON serializable" 28 | ) 29 | 30 | with open(f"{name}.json", "w") as outfile: 31 | json.dump(content, outfile, indent=2, default=default_serializer) 32 | 33 | 34 | def current_date_time_str() -> str: 35 | return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 36 | 37 | 38 | def current_date_str() -> str: 39 | return datetime.datetime.now().strftime("%Y-%m-%d") 40 | 41 | 42 | def dict_item_diff_by_set( 43 | previous_list: List[Dict], current_list: List[Dict], set_key: str 44 | ) -> List[str]: 45 | previous_set = {item[set_key] for item in previous_list} 46 | current_set = {item[set_key] for item in current_list} 47 | return list(current_set - previous_set) 48 | 49 | 50 | def create_session_logger_id() -> str: 51 | return ( 52 | datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:6] 53 | ) 54 | 55 | 56 | import logging 57 | import sys 58 | 59 | 60 | def setup_logging(session_id: str): 61 | """Configure logging with session-specific log file and stdout""" 62 | log_file = build_file_name_session("session.log", session_id) 63 | 64 | # Create a new logger specific to our application 65 | logger = logging.getLogger("main") 66 | logger.setLevel(logging.INFO) 67 | 68 | # Clear any existing handlers to avoid duplicates 69 | logger.handlers.clear() 70 | 71 | # Create formatter with emoji mapping 72 | class EmojiFormatter(logging.Formatter): 73 | EMOJI_MAP = { 74 | logging.INFO: "ℹ️", 75 | logging.WARNING: "⚠️", 76 | logging.ERROR: "❌", 77 | logging.CRITICAL: "🔥", 78 | logging.DEBUG: "🐛" 79 | } 80 | 81 | def format(self, record): 82 | # Skip stdout for messages with skip_stdout flag 83 | if hasattr(record, 'skip_stdout') and record.skip_stdout: 84 | return "" 85 | 86 | emoji = self.EMOJI_MAP.get(record.levelno, "📝") 87 | self._style._fmt = f"{emoji} %(asctime)s - %(levelname)s - %(message)s" 88 | return super().format(record) 89 | 90 | # Create file handler 91 | file_handler = logging.FileHandler(log_file) 92 | file_handler.setFormatter(EmojiFormatter()) 93 | 94 | # Create stdout handler with filter 95 | stdout_handler = logging.StreamHandler(sys.stdout) 96 | stdout_formatter = EmojiFormatter() 97 | stdout_handler.setFormatter(stdout_formatter) 98 | 99 | # Add filter to skip messages with skip_stdout flag 100 | stdout_handler.addFilter(lambda record: not getattr(record, 'skip_stdout', False)) 101 | 102 | # Add both handlers 103 | logger.addHandler(file_handler) 104 | logger.addHandler(stdout_handler) 105 | 106 | return logger 107 | 108 | 109 | def parse_markdown_backticks(str) -> str: 110 | if "```" not in str: 111 | return str.strip() 112 | # Remove opening backticks and language identifier 113 | str = str.split("```", 1)[-1].split("\n", 1)[-1] 114 | # Remove closing backticks 115 | str = str.rsplit("```", 1)[0] 116 | # Remove any leading or trailing whitespace 117 | return str.strip() 118 | -------------------------------------------------------------------------------- /main_typer_assistant.py: -------------------------------------------------------------------------------- 1 | from RealtimeSTT import AudioToTextRecorder 2 | from modules.assistant_config import get_config 3 | from modules.typer_agent import TyperAgent 4 | from modules.utils import create_session_logger_id, setup_logging 5 | import logging 6 | import typer 7 | from typing import List 8 | import os 9 | 10 | app = typer.Typer() 11 | 12 | 13 | @app.command() 14 | def ping(): 15 | print("pong") 16 | 17 | 18 | @app.command() 19 | def awaken( 20 | typer_file: str = typer.Option( 21 | ..., "--typer-file", "-f", help="Path to typer commands file" 22 | ), 23 | scratchpad: str = typer.Option( 24 | ..., "--scratchpad", "-s", help="Path to scratchpad file" 25 | ), 26 | context_files: List[str] = typer.Option( 27 | [], "--context", "-c", help="List of context files" 28 | ), 29 | mode: str = typer.Option( 30 | "default", 31 | "--mode", 32 | "-m", 33 | help="Options: ('default', 'execute', 'execute-no-scratch'). Execution mode: default (no exec), execute (exec + scratch), execute-no-scratch (exec only)", 34 | ), 35 | ): 36 | """Run STT interface that processes speech into typer commands""" 37 | # Remove the list concatenation - pass scratchpad as a single string 38 | assistant, typer_file, _ = TyperAgent.build_agent(typer_file, [scratchpad]) 39 | 40 | print("🎤 Speak now... (press Ctrl+C to exit)") 41 | 42 | recorder = AudioToTextRecorder( 43 | spinner=False, 44 | # wake_words="deep" # specific wake words to trigger the assistant using the realtime-stt library. we do this manually below so we can use any word. 45 | # realtime_processing_pause=0.3, 46 | post_speech_silence_duration=1.5, # how long to wait after speech ends before processing 47 | # compute_type="int8", 48 | compute_type="float32", 49 | model="tiny.en", # VERY fast (.5s), but not accurate 50 | # model="small.en", # decent speed (1.5s), improved accuracy 51 | # Beam size controls how many alternative transcription paths are explored 52 | # Higher values = more accurate but slower, lower values = faster but less accurate 53 | # beam_size=3, 54 | # beam_size=5, 55 | beam_size=8, 56 | # Batch size controls how many audio chunks are processed together 57 | # Higher values = faster processing but uses more memory, lower values = slower processing but uses less memory 58 | batch_size=25, 59 | # model="large-v3", # very slow, but accurate 60 | # model="distil-large-v3", # very slow (but faster than large-v3) but accurate 61 | # realtime_model_type="tiny.en", # realtime models are used for the on_realtime_transcription_update() callback 62 | # realtime_model_type="large-v3", 63 | language="en", 64 | print_transcription_time=True, 65 | # enable_realtime_transcription=True, 66 | # on_realtime_transcription_update=lambda text: print( 67 | # f"🎤 on_realtime_transcription_update(): {text}" 68 | # ), 69 | # on_realtime_transcription_stabilized=lambda text: print( 70 | # f"🎤 on_realtime_transcription_stabilized(): {text}" 71 | # ), 72 | # on_recorded_chunk=lambda chunk: print(f"🎤 on_recorded_chunk(): {chunk}"), 73 | # on_transcription_start=lambda: print("🎤 on_transcription_start()"), 74 | # on_recording_stop=lambda: print("🎤 on_transcription_stop()"), 75 | # on_recording_start=lambda: print("🎤 on_recording_start()"), 76 | ) 77 | 78 | def process_text(text): 79 | print(f"\n🎤 Heard: {text}") 80 | try: 81 | assistant_name = get_config("typer_assistant.assistant_name") 82 | if assistant_name.lower() not in text.lower(): 83 | print(f"🤖 Not {assistant_name} - ignoring") 84 | return 85 | 86 | recorder.stop() 87 | output = assistant.process_text( 88 | text, typer_file, scratchpad, context_files, mode 89 | ) 90 | print(f"🤖 Response:\n{output}") 91 | recorder.start() 92 | except Exception as e: 93 | print(f"❌ Error: {str(e)}") 94 | 95 | while True: 96 | recorder.text(process_text) 97 | 98 | 99 | if __name__ == "__main__": 100 | app() 101 | -------------------------------------------------------------------------------- /modules/base_assistant.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | import logging 3 | import os 4 | from modules.deepseek import conversational_prompt as deepseek_conversational_prompt 5 | from modules.ollama import conversational_prompt as ollama_conversational_prompt 6 | from modules.utils import build_file_name_session 7 | from RealtimeTTS import TextToAudioStream, SystemEngine 8 | from elevenlabs import play 9 | from elevenlabs.client import ElevenLabs 10 | import pyttsx3 11 | import time 12 | from modules.assistant_config import get_config 13 | 14 | 15 | class PlainAssistant: 16 | def __init__(self, logger: logging.Logger, session_id: str): 17 | self.logger = logger 18 | self.session_id = session_id 19 | self.conversation_history = [] 20 | 21 | # Get voice configuration 22 | self.voice_type = get_config("base_assistant.voice") 23 | self.elevenlabs_voice = get_config("base_assistant.elevenlabs_voice") 24 | self.brain = get_config("base_assistant.brain") 25 | 26 | # Initialize appropriate TTS engine 27 | if self.voice_type == "local": 28 | self.logger.info("🔊 Initializing local TTS engine") 29 | self.engine = pyttsx3.init() 30 | self.engine.setProperty("rate", 150) # Speed of speech 31 | self.engine.setProperty("volume", 1.0) # Volume level 32 | elif self.voice_type == "realtime-tts": 33 | self.logger.info("🔊 Initializing RealtimeTTS engine") 34 | self.engine = SystemEngine() 35 | self.stream = TextToAudioStream( 36 | self.engine, frames_per_buffer=256, playout_chunk_size=1024 37 | ) 38 | elif self.voice_type == "elevenlabs": 39 | self.logger.info("🔊 Initializing ElevenLabs TTS engine") 40 | self.elevenlabs_client = ElevenLabs(api_key=os.getenv("ELEVEN_API_KEY")) 41 | else: 42 | raise ValueError(f"Unsupported voice type: {self.voice_type}") 43 | 44 | def process_text(self, text: str) -> str: 45 | """Process text input and generate response""" 46 | try: 47 | # Check if text matches our last response 48 | if ( 49 | self.conversation_history 50 | and text.strip().lower() 51 | in self.conversation_history[-1]["content"].lower() 52 | ): 53 | self.logger.info("🤖 Ignoring own speech input") 54 | return "" 55 | 56 | # Add user message to conversation history 57 | self.conversation_history.append({"role": "user", "content": text}) 58 | 59 | # Generate response using configured brain 60 | self.logger.info(f"🤖 Processing text with {self.brain}...") 61 | if self.brain.startswith("ollama:"): 62 | model_no_prefix = ":".join(self.brain.split(":")[1:]) 63 | response = ollama_conversational_prompt( 64 | self.conversation_history, model=model_no_prefix 65 | ) 66 | else: 67 | response = deepseek_conversational_prompt(self.conversation_history) 68 | 69 | # Add assistant response to history 70 | self.conversation_history.append({"role": "assistant", "content": response}) 71 | 72 | # Speak the response 73 | self.speak(response) 74 | 75 | return response 76 | 77 | except Exception as e: 78 | self.logger.error(f"❌ Error occurred: {str(e)}") 79 | raise 80 | 81 | def speak(self, text: str): 82 | """Convert text to speech using configured engine""" 83 | try: 84 | self.logger.info(f"🔊 Speaking: {text}") 85 | 86 | if self.voice_type == "local": 87 | self.engine.say(text) 88 | self.engine.runAndWait() 89 | 90 | elif self.voice_type == "realtime-tts": 91 | self.stream.feed(text) 92 | self.stream.play() 93 | 94 | elif self.voice_type == "elevenlabs": 95 | audio = self.elevenlabs_client.generate( 96 | text=text, 97 | voice=self.elevenlabs_voice, 98 | model="eleven_turbo_v2", 99 | stream=False, 100 | ) 101 | play(audio) 102 | 103 | self.logger.info(f"🔊 Spoken: {text}") 104 | 105 | except Exception as e: 106 | self.logger.error(f"❌ Error in speech synthesis: {str(e)}") 107 | raise 108 | -------------------------------------------------------------------------------- /scratchpad.md: -------------------------------------------------------------------------------- 1 | # Personal AI Assistant Scratchpad 2 | 3 | ## Ada Executed Command (2025-01-11 13:31:51) 4 | 5 | > Request: Ada, list users that are viewers. 6 | 7 | **Ada's Command:** 8 | ```bash 9 | uv run python commands/template.py list-users --role viewer 10 | ``` 11 | 12 | **Output:** 13 | ``` 14 | Users: 15 | - Tim (Role: viewer, Created: 2025-01-11T11:10:00.857285) 16 | - user_11 (Role: viewer, Created: 2025-01-11T10:14:21.118499) 17 | - user_17 (Role: viewer, Created: 2025-01-11T10:14:21.118514) 18 | - user_21 (Role: viewer, Created: 2025-01-11T10:14:21.118524) 19 | - user_22 (Role: viewer, Created: 2025-01-11T10:14:21.118526) 20 | - user_4 (Role: viewer, Created: 2025-01-11T10:14:21.118476) 21 | - user_9 (Role: viewer, Created: 2025-01-11T10:14:21.118494) 22 | 23 | ``` 24 | 25 | ## Update User Block 26 | 27 | ### Create User Block 28 | Alex - viewer 29 | Mary - editor 30 | Steve - admin 31 | 32 | ### Delete User Block 33 | user_11 34 | user_22 35 | user_4 36 | 37 | ## Ada Executed Command (2025-01-11 13:34:50) 38 | 39 | > Request: Ada, go ahead and look at the update user block and run the respective create user and delete user commands. 40 | 41 | **Ada's Command:** 42 | ```bash 43 | uv run python commands/template.py create-user Alex --role viewer && uv run python commands/template.py create-user Mary --role editor && uv run python commands/template.py create-user Steve --role admin && uv run python commands/template.py delete-user user_11 --confirm && uv run python commands/template.py delete-user user_22 --confirm && uv run python commands/template.py delete-user user_4 --confirm 44 | ``` 45 | 46 | **Output:** 47 | ``` 48 | User 'Alex' created with role 'viewer'. 49 | User 'Mary' created with role 'editor'. 50 | User 'Steve' created with role 'admin'. 51 | No user found with ID user_11. 52 | No user found with ID user_22. 53 | No user found with ID user_4. 54 | ``` 55 | 56 | ## Ada Executed Command (2025-01-11 13:35:51) 57 | 58 | > Request: Okay, so let's check this out. So this is really cool, right? So she created the command. She created this giant execution statement and we got a couple of outputs here. We have, you know, our creatives, our creative commands and then we also have our delete commands that failed. So something went wrong here and I think it's because let me look at exactly how this delete user works. user ID might just be a single ID. So yeah, that looks right. So what I'm going to do is I'm just going to ask Ada to correct this for us, right? So I'll say Ada, rerun the delete commands, but only use the actual user ID. So not the user underscore prefix. Just use the numbers for the deletion. 59 | 60 | **Ada's Command:** 61 | ```bash 62 | uv run python commands/template.py delete-user 11 --confirm && uv run python commands/template.py delete-user 22 --confirm && uv run python commands/template.py delete-user 4 --confirm 63 | ``` 64 | 65 | **Output:** 66 | ``` 67 | User with ID 11 deleted. 68 | User with ID 22 deleted. 69 | User with ID 4 deleted. 70 | ``` 71 | 72 | ## Ada Executed Command (2025-01-11 13:43:27) 73 | 74 | > Request: Get some content here for Ada to transcribe for a personal AI assistant to transcribe. It's coming in the ears, which is powered by real-time speech-to-text. And then if we pause for a moment, the transcription process will actually kick off. And notice how much slower this is than our previous executions. 75 | 76 | **Ada's Command:** 77 | ```bash 78 | uv run python commands/template.py list-users --role viewer 79 | ``` 80 | 81 | **Output:** 82 | ``` 83 | Users: 84 | - Alex (Role: viewer, Created: 2025-01-11T13:34:50.762624) 85 | - Tim (Role: viewer, Created: 2025-01-11T11:10:00.857285) 86 | - user_11 (Role: viewer, Created: 2025-01-11T10:14:21.118499) 87 | - user_17 (Role: viewer, Created: 2025-01-11T10:14:21.118514) 88 | - user_22 (Role: viewer, Created: 2025-01-11T10:14:21.118526) 89 | - user_4 (Role: viewer, Created: 2025-01-11T10:14:21.118476) 90 | - user_9 (Role: viewer, Created: 2025-01-11T10:14:21.118494) 91 | 92 | ``` 93 | 94 | ## Ada Executed Command (2025-01-12 12:39:28) 95 | 96 | > Request: ADA, ping the server. 97 | 98 | **Ada's Command:** 99 | ```bash 100 | uv run python commands/template.py ping-server 101 | ``` 102 | 103 | **Output:** 104 | ``` 105 | Server pinged. Response time: 276 ms. 106 | ``` 107 | 108 | ## Ada Executed Command (2025-01-12 12:46:24) 109 | 110 | > Request: Hello, ADA, ping the server, wait for a response. 111 | 112 | **Ada's Command:** 113 | ```bash 114 | uv run python commands/template.py ping-server --wait 115 | ``` 116 | 117 | **Output:** 118 | ``` 119 | Server pinged. Response time: 211 ms. (Waited for a response.) 120 | ``` -------------------------------------------------------------------------------- /modules/deepseek.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | import os 3 | import json 4 | from dotenv import load_dotenv 5 | from typing import List, Dict 6 | 7 | # Load environment variables 8 | load_dotenv() 9 | 10 | # Initialize DeepSeek client 11 | client = OpenAI( 12 | api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com/beta" 13 | ) 14 | 15 | DEEPSEEK_V3_MODEL = "deepseek-chat" 16 | 17 | 18 | def prompt(prompt: str, model: str = DEEPSEEK_V3_MODEL) -> str: 19 | """ 20 | Send a prompt to DeepSeek and get detailed benchmarking response. 21 | """ 22 | response = client.chat.completions.create( 23 | model=model, messages=[{"role": "user", "content": prompt}], stream=False 24 | ) 25 | return response.choices[0].message.content 26 | 27 | 28 | def fill_in_the_middle_prompt( 29 | prompt: str, suffix: str, model: str = DEEPSEEK_V3_MODEL 30 | ) -> str: 31 | """ 32 | Send a fill-in-the-middle prompt to DeepSeek and get response. 33 | 34 | The max tokens of FIM completion is 4K. 35 | 36 | example: 37 | prompt="def fib(a):", 38 | suffix=" return fib(a-1) + fib(a-2)", 39 | """ 40 | response = client.completions.create(model=model, prompt=prompt, suffix=suffix) 41 | return prompt + response.choices[0].text + suffix 42 | 43 | 44 | def json_prompt(prompt: str, model: str = DEEPSEEK_V3_MODEL) -> dict: 45 | """ 46 | Send a prompt to DeepSeek and get JSON response. 47 | 48 | Args: 49 | prompt: The user prompt to send 50 | system_prompt: Optional system prompt to set context 51 | model: The model to use, defaults to deepseek-chat 52 | 53 | Returns: 54 | dict: The parsed JSON response 55 | """ 56 | messages = [{"role": "user", "content": prompt}] 57 | 58 | response = client.chat.completions.create( 59 | model=model, messages=messages, response_format={"type": "json_object"} 60 | ) 61 | return json.loads(response.choices[0].message.content) 62 | 63 | 64 | def prefix_prompt( 65 | prompt: str, prefix: str, model: str = DEEPSEEK_V3_MODEL, no_prefix: bool = False 66 | ) -> str: 67 | """ 68 | Send a prompt to DeepSeek with a prefix constraint and get 'prefix + response' 69 | 70 | Args: 71 | prompt: The user prompt to send 72 | prefix: The required prefix for the response 73 | model: The model to use, defaults to deepseek-chat 74 | no_prefix: If True, the prefix is not added to the response 75 | Returns: 76 | str: The model's response constrained by the prefix 77 | """ 78 | messages = [ 79 | {"role": "user", "content": prompt}, 80 | {"role": "assistant", "content": prefix, "prefix": True}, 81 | ] 82 | 83 | response = client.chat.completions.create(model=model, messages=messages) 84 | if no_prefix: 85 | return response.choices[0].message.content 86 | else: 87 | return prefix + response.choices[0].message.content 88 | 89 | 90 | def prefix_then_stop_prompt( 91 | prompt: str, prefix: str, suffix: str, model: str = DEEPSEEK_V3_MODEL 92 | ) -> str: 93 | """ 94 | Send a prompt to DeepSeek with a prefix and suffix constraint and get 'response' only that will have started with prefix and ended with suffix 95 | 96 | Args: 97 | prompt: The user prompt to send 98 | prefix: The required prefix for the response 99 | suffix: The required suffix for the response 100 | model: The model to use, defaults to deepseek-chat 101 | 102 | Returns: 103 | str: The model's response constrained by the prefix and suffix 104 | """ 105 | messages = [ 106 | {"role": "user", "content": prompt}, 107 | {"role": "assistant", "content": prefix, "prefix": True}, 108 | ] 109 | response = client.chat.completions.create( 110 | model=model, messages=messages, stop=[suffix] 111 | ) 112 | return response.choices[0].message.content 113 | # return prefix + response.choices[0].message.content 114 | 115 | 116 | def conversational_prompt( 117 | messages: List[Dict[str, str]], 118 | system_prompt: str = "You are a helpful conversational assistant. Respond in a short, concise, friendly manner.", 119 | model: str = DEEPSEEK_V3_MODEL, 120 | ) -> str: 121 | """ 122 | Send a conversational prompt to DeepSeek with message history. 123 | 124 | Args: 125 | messages: List of message dicts with 'role' and 'content' keys 126 | model: The model to use, defaults to deepseek-chat 127 | 128 | Returns: 129 | str: The model's response 130 | """ 131 | try: 132 | messages = [ 133 | {"role": "system", "content": system_prompt}, 134 | *messages, 135 | ] 136 | response = client.chat.completions.create( 137 | model=model, messages=messages, stream=False 138 | ) 139 | return response.choices[0].message.content 140 | except Exception as e: 141 | raise Exception(f"Error in conversational prompt: {str(e)}") 142 | -------------------------------------------------------------------------------- /modules/typer_agent.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import os 3 | import logging 4 | from datetime import datetime 5 | from modules.assistant_config import get_config 6 | from modules.utils import ( 7 | build_file_name_session, 8 | create_session_logger_id, 9 | setup_logging, 10 | ) 11 | from modules.deepseek import prefix_prompt 12 | from modules.execute_python import execute_uv_python, execute 13 | from elevenlabs import play 14 | from elevenlabs.client import ElevenLabs 15 | import time 16 | 17 | 18 | class TyperAgent: 19 | def __init__(self, logger: logging.Logger, session_id: str): 20 | self.logger = logger 21 | self.session_id = session_id 22 | self.log_file = build_file_name_session("session.log", session_id) 23 | self.elevenlabs_client = ElevenLabs(api_key=os.getenv("ELEVEN_API_KEY")) 24 | self.previous_successful_requests = [] 25 | self.previous_responses = [] 26 | 27 | def _validate_markdown(self, file_path: str) -> bool: 28 | """Validate that file is markdown and has expected structure""" 29 | if not file_path.endswith((".md", ".markdown")): 30 | self.logger.error(f"📄 Scratchpad file {file_path} must be a markdown file") 31 | return False 32 | 33 | try: 34 | with open(file_path, "r") as f: 35 | content = f.read() 36 | # Basic validation - could be expanded based on needs 37 | if not content.strip(): 38 | self.logger.warning("📄 Markdown file is empty") 39 | return True 40 | except Exception as e: 41 | self.logger.error(f"📄 Error reading markdown file: {str(e)}") 42 | return False 43 | 44 | @classmethod 45 | def build_agent(cls, typer_file: str, scratchpad: List[str]): 46 | """Create and configure a new TyperAssistant instance""" 47 | session_id = create_session_logger_id() 48 | logger = setup_logging(session_id) 49 | logger.info(f"🚀 Starting STT session {session_id}") 50 | 51 | if not os.path.exists(typer_file): 52 | logger.error(f"📂 Typer file {typer_file} does not exist") 53 | raise FileNotFoundError(f"Typer file {typer_file} does not exist") 54 | 55 | # Validate markdown scratchpad 56 | agent = cls(logger, session_id) 57 | if scratchpad and not agent._validate_markdown(scratchpad[0]): 58 | raise ValueError(f"Invalid markdown scratchpad file: {scratchpad[0]}") 59 | 60 | return agent, typer_file, scratchpad[0] 61 | 62 | def build_prompt( 63 | self, 64 | typer_file: str, 65 | scratchpad: str, 66 | context_files: List[str], 67 | prompt_text: str, 68 | ) -> str: 69 | """Build and format the prompt template with current state""" 70 | try: 71 | # Load typer file 72 | self.logger.info("📂 Loading typer file...") 73 | with open(typer_file, "r") as f: 74 | typer_content = f.read() 75 | 76 | # Load scratchpad file 77 | self.logger.info("📝 Loading scratchpad file...") 78 | if not os.path.exists(scratchpad): 79 | self.logger.error(f"📄 Scratchpad file {scratchpad} does not exist") 80 | raise FileNotFoundError(f"Scratchpad file {scratchpad} does not exist") 81 | 82 | with open(scratchpad, "r") as f: 83 | scratchpad_content = f.read() 84 | 85 | # Load context files 86 | context_content = "" 87 | for file_path in context_files: 88 | if not os.path.exists(file_path): 89 | self.logger.error(f"📄 Context file {file_path} does not exist") 90 | raise FileNotFoundError(f"Context file {file_path} does not exist") 91 | 92 | with open(file_path, "r") as f: 93 | file_content = f.read() 94 | file_name = os.path.basename(file_path) 95 | context_content += f'\t\n{file_content}\n\n\n' 96 | 97 | # Load and format prompt template 98 | self.logger.info("📝 Loading prompt template...") 99 | with open("prompts/typer-commands.xml", "r") as f: 100 | prompt_template = f.read() 101 | 102 | # Replace template placeholders 103 | formatted_prompt = ( 104 | prompt_template.replace("{{typer-commands}}", typer_content) 105 | .replace("{{scratch_pad}}", scratchpad_content) 106 | .replace("{{context_files}}", context_content) 107 | .replace("{{natural_language_request}}", prompt_text) 108 | ) 109 | 110 | # Log the filled prompt template to file only (not stdout) 111 | with open(self.log_file, "a") as log: 112 | log.write("\n📝 Filled prompt template:\n") 113 | log.write(formatted_prompt) 114 | log.write("\n\n") 115 | 116 | return formatted_prompt 117 | 118 | except Exception as e: 119 | self.logger.error(f"❌ Error building prompt: {str(e)}") 120 | raise 121 | 122 | def process_text( 123 | self, 124 | text: str, 125 | typer_file: str, 126 | scratchpad: str, 127 | context_files: List[str], 128 | mode: str, 129 | ) -> str: 130 | """Process text input and handle based on execution mode""" 131 | try: 132 | # Build fresh prompt with current state 133 | formatted_prompt = self.build_prompt( 134 | typer_file, scratchpad, context_files, text 135 | ) 136 | 137 | # Generate command using DeepSeek 138 | self.logger.info("🤖 Processing text with DeepSeek...") 139 | prefix = f"uv run python {typer_file}" 140 | command = prefix_prompt(prompt=formatted_prompt, prefix=prefix) 141 | 142 | if command == prefix.strip(): 143 | self.logger.info(f"🤖 Command not found for '{text}'") 144 | self.speak("I couldn't find that command") 145 | return "Command not found" 146 | 147 | # Handle different modes with markdown formatting 148 | assistant_name = get_config("typer_assistant.assistant_name") 149 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 150 | 151 | # command_with_prefix = f"uv run python {typer_file} {command}" 152 | command_with_prefix = command 153 | 154 | if mode == "default": 155 | result = ( 156 | f"\n## {assistant_name} Generated Command ({timestamp})\n\n" 157 | f"> Request: {text}\n\n" 158 | f"```bash\n{command_with_prefix}\n```" 159 | ) 160 | with open(scratchpad, "a") as f: 161 | f.write(result) 162 | self.think_speak(f"Command generated") 163 | return result 164 | 165 | elif mode == "execute": 166 | self.logger.info(f"⚡ Executing command: `{command_with_prefix}`") 167 | output = execute(command) 168 | 169 | result = ( 170 | f"\n\n## {assistant_name} Executed Command ({timestamp})\n\n" 171 | f"> Request: {text}\n\n" 172 | f"**{assistant_name}'s Command:** \n```bash\n{command_with_prefix}\n```\n\n" 173 | f"**Output:** \n```\n{output}```" 174 | ) 175 | with open(scratchpad, "a") as f: 176 | f.write(result) 177 | self.think_speak(f"Command generated and executed") 178 | return output 179 | 180 | elif mode == "execute-no-scratch": 181 | self.logger.info(f"⚡ Executing command: `{command_with_prefix}`") 182 | output = execute(command) 183 | self.think_speak(f"Command generated and executed") 184 | return output 185 | 186 | else: 187 | self.think_speak(f"I had trouble running that command") 188 | raise ValueError(f"Invalid mode: {mode}") 189 | 190 | except Exception as e: 191 | self.logger.error(f"❌ Error occurred: {str(e)}") 192 | raise 193 | 194 | def think_speak(self, text: str): 195 | response_prompt_base = "" 196 | with open("prompts/concise-assistant-response.xml", "r") as f: 197 | response_prompt_base = f.read() 198 | 199 | assistant_name = get_config("typer_assistant.assistant_name") 200 | human_companion_name = get_config("typer_assistant.human_companion_name") 201 | 202 | response_prompt = response_prompt_base.replace("{{latest_action}}", text) 203 | response_prompt = response_prompt.replace( 204 | "{{human_companion_name}}", human_companion_name 205 | ) 206 | response_prompt = response_prompt.replace( 207 | "{{personal_ai_assistant_name}}", assistant_name 208 | ) 209 | prompt_prefix = f"Your Conversational Response: " 210 | response = prefix_prompt( 211 | prompt=response_prompt, prefix=prompt_prefix, no_prefix=True 212 | ) 213 | self.logger.info(f"🤖 Response: '{response}'") 214 | self.speak(response) 215 | 216 | def speak(self, text: str): 217 | 218 | start_time = time.time() 219 | model = "eleven_flash_v2_5" 220 | # model="eleven_flash_v2" 221 | # model = "eleven_turbo_v2" 222 | # model = "eleven_turbo_v2_5" 223 | # model="eleven_multilingual_v2" 224 | voice = get_config("typer_assistant.elevenlabs_voice") 225 | 226 | audio_generator = self.elevenlabs_client.generate( 227 | text=text, 228 | voice=voice, 229 | model=model, 230 | stream=False, 231 | ) 232 | audio_bytes = b"".join(list(audio_generator)) 233 | duration = time.time() - start_time 234 | self.logger.info(f"Model {model} completed tts in {duration:.2f} seconds") 235 | play(audio_bytes) 236 | -------------------------------------------------------------------------------- /commands/template_empty.py: -------------------------------------------------------------------------------- 1 | import typer 2 | from typing import Optional 3 | 4 | app = typer.Typer() 5 | 6 | 7 | @app.command() 8 | def ping_server( 9 | wait: bool = typer.Option(False, "--wait", help="Wait for server response?") 10 | ): 11 | """ 12 | Pings the server, optionally waiting for a response. 13 | """ 14 | pass 15 | 16 | 17 | @app.command() 18 | def show_config( 19 | verbose: bool = typer.Option(False, "--verbose", help="Show config in detail?") 20 | ): 21 | """ 22 | Shows the current configuration. 23 | """ 24 | pass 25 | 26 | 27 | @app.command() 28 | def list_files( 29 | path: str = typer.Argument(..., help="Path to list files from"), 30 | all_files: bool = typer.Option(False, "--all", help="Include hidden files"), 31 | ): 32 | """ 33 | Lists files in a directory. Optionally show hidden files. 34 | """ 35 | pass 36 | 37 | 38 | @app.command() 39 | def create_user( 40 | username: str = typer.Argument(..., help="Name of the new user"), 41 | role: str = typer.Option("guest", "--role", help="Role for the new user"), 42 | ): 43 | """ 44 | Creates a new user with an optional role. 45 | """ 46 | pass 47 | 48 | 49 | @app.command() 50 | def delete_user( 51 | user_id: str = typer.Argument(..., help="ID of user to delete"), 52 | confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation prompt"), 53 | ): 54 | """ 55 | Deletes a user by ID. 56 | """ 57 | pass 58 | 59 | 60 | @app.command() 61 | def generate_report( 62 | report_type: str = typer.Argument(..., help="Type of report to generate"), 63 | output_file: str = typer.Option("report.json", "--output", help="Output file name"), 64 | ): 65 | """ 66 | Generates a report of a specified type to a given file. 67 | """ 68 | pass 69 | 70 | 71 | @app.command() 72 | def backup_data( 73 | directory: str = typer.Argument(..., help="Directory to store backups"), 74 | full: bool = typer.Option(False, "--full", help="Perform a full backup"), 75 | ): 76 | """ 77 | Back up data to a specified directory, optionally performing a full backup. 78 | """ 79 | pass 80 | 81 | 82 | @app.command() 83 | def restore_data( 84 | file_path: str = typer.Argument(..., help="File path of backup to restore"), 85 | overwrite: bool = typer.Option( 86 | False, "--overwrite", help="Overwrite existing data" 87 | ), 88 | ): 89 | """ 90 | Restores data from a backup file. 91 | """ 92 | pass 93 | 94 | 95 | @app.command() 96 | def summarize_logs( 97 | logs_path: str = typer.Argument(..., help="Path to log files"), 98 | lines: int = typer.Option(100, "--lines", help="Number of lines to summarize"), 99 | ): 100 | """ 101 | Summarizes log data from a specified path, limiting lines. 102 | """ 103 | pass 104 | 105 | 106 | @app.command() 107 | def upload_file( 108 | file_path: str = typer.Argument(..., help="Path of file to upload"), 109 | destination: str = typer.Option( 110 | "remote", "--destination", help="Destination label" 111 | ), 112 | secure: bool = typer.Option(True, "--secure", help="Use secure upload"), 113 | ): 114 | """ 115 | Uploads a file to a destination, optionally enforcing secure upload. 116 | """ 117 | pass 118 | 119 | 120 | @app.command() 121 | def download_file( 122 | url: str = typer.Argument(..., help="URL of file to download"), 123 | output_path: str = typer.Option(".", "--output", help="Local output path"), 124 | retry: int = typer.Option(3, "--retry", help="Number of times to retry"), 125 | ): 126 | """ 127 | Downloads a file from a URL with a specified number of retries. 128 | """ 129 | pass 130 | 131 | 132 | @app.command() 133 | def filter_records( 134 | source: str = typer.Argument(..., help="Data source to filter"), 135 | query: str = typer.Option("", "--query", help="Filtering query string"), 136 | limit: int = typer.Option(10, "--limit", help="Limit the number of results"), 137 | ): 138 | """ 139 | Filters records from a data source using a query, limiting the number of results. 140 | """ 141 | pass 142 | 143 | 144 | @app.command() 145 | def validate_schema( 146 | schema_file: str = typer.Argument(..., help="Path to schema file"), 147 | data_file: str = typer.Option("", "--data", help="Path to data file to check"), 148 | strict: bool = typer.Option(True, "--strict", help="Enforce strict validation"), 149 | ): 150 | """ 151 | Validates a schema, optionally checking a data file with strict mode. 152 | """ 153 | pass 154 | 155 | 156 | @app.command() 157 | def sync_remotes( 158 | remote_name: str = typer.Argument(..., help="Name of remote to sync"), 159 | force: bool = typer.Option( 160 | False, "--force", help="Force syncing without prompting" 161 | ), 162 | ): 163 | """ 164 | Syncs with a remote repository, optionally forcing the operation. 165 | """ 166 | pass 167 | 168 | 169 | @app.command() 170 | def simulate_run( 171 | scenario: str = typer.Argument(..., help="Simulation scenario"), 172 | cycles: int = typer.Option(5, "--cycles", help="Number of cycles to simulate"), 173 | debug: bool = typer.Option(False, "--debug", help="Show debug output"), 174 | ): 175 | """ 176 | Simulates a scenario for a given number of cycles, optionally showing debug output. 177 | """ 178 | pass 179 | 180 | 181 | @app.command() 182 | def compare_files( 183 | file_a: str = typer.Argument(..., help="First file to compare"), 184 | file_b: str = typer.Argument(..., help="Second file to compare"), 185 | diff_only: bool = typer.Option( 186 | False, "--diff-only", help="Show only the differences" 187 | ), 188 | ): 189 | """ 190 | Compares two files, optionally showing only differences. 191 | """ 192 | pass 193 | 194 | 195 | @app.command() 196 | def encrypt_data( 197 | input_path: str = typer.Argument(..., help="Path of the file to encrypt"), 198 | output_path: str = typer.Option("encrypted.bin", "--output", help="Output file"), 199 | algorithm: str = typer.Option("AES", "--algorithm", help="Encryption algorithm"), 200 | ): 201 | """ 202 | Encrypts data using a specified algorithm and writes to an output file. 203 | """ 204 | pass 205 | 206 | 207 | @app.command() 208 | def decrypt_data( 209 | encrypted_file: str = typer.Argument(..., help="Path to encrypted file"), 210 | key: str = typer.Option(..., "--key", help="Decryption key"), 211 | output_path: str = typer.Option("decrypted.txt", "--output", help="Output file"), 212 | ): 213 | """ 214 | Decrypts an encrypted file using a key. 215 | """ 216 | pass 217 | 218 | 219 | @app.command() 220 | def transform_data( 221 | input_file: str = typer.Argument(..., help="File to transform"), 222 | output_format: str = typer.Option("json", "--format", help="Output format"), 223 | columns: str = typer.Option( 224 | None, "--columns", help="Comma-separated columns to extract" 225 | ), 226 | ): 227 | """ 228 | Transforms data from a file into a specified format, optionally extracting columns. 229 | """ 230 | pass 231 | 232 | 233 | @app.command() 234 | def upload_changes( 235 | source_dir: str = typer.Argument(..., help="Directory of changes to upload"), 236 | incremental: bool = typer.Option(False, "--incremental", help="Incremental upload"), 237 | confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation prompt"), 238 | ): 239 | """ 240 | Uploads changes from a directory, optionally in incremental mode. 241 | """ 242 | pass 243 | 244 | 245 | @app.command() 246 | def migrate_database( 247 | old_db: str = typer.Argument(..., help="Path to old database"), 248 | new_db: str = typer.Option(..., "--new-db", help="Path to new database"), 249 | dry_run: bool = typer.Option( 250 | False, "--dry-run", help="Perform a trial run without changing data" 251 | ), 252 | ): 253 | """ 254 | Migrates data from an old database to a new one, optionally doing a dry run. 255 | """ 256 | pass 257 | 258 | 259 | @app.command() 260 | def health_check( 261 | service_name: str = typer.Argument(..., help="Service to check"), 262 | timeout: int = typer.Option(30, "--timeout", help="Timeout in seconds"), 263 | alert: bool = typer.Option(False, "--alert", help="Send alert if check fails"), 264 | ): 265 | """ 266 | Checks the health of a service within a specified timeout, optionally sending alerts. 267 | """ 268 | pass 269 | 270 | 271 | @app.command() 272 | def search_logs( 273 | keyword: str = typer.Argument(..., help="Keyword to search"), 274 | log_file: str = typer.Option("system.log", "--log", help="Log file to search in"), 275 | case_sensitive: bool = typer.Option( 276 | False, "--case-sensitive", help="Enable case-sensitive search" 277 | ), 278 | ): 279 | """ 280 | Searches for a keyword in a log file, optionally using case-sensitive mode. 281 | """ 282 | pass 283 | 284 | 285 | @app.command() 286 | def stats_by_date( 287 | date: str = typer.Argument(..., help="Date in YYYY-MM-DD to query stats"), 288 | show_raw: bool = typer.Option(False, "--show-raw", help="Display raw data"), 289 | ): 290 | """ 291 | Shows statistics for a specific date, optionally displaying raw data. 292 | """ 293 | pass 294 | 295 | 296 | @app.command() 297 | def publish_update( 298 | version: str = typer.Argument(..., help="Version tag to publish"), 299 | channel: str = typer.Option("stable", "--channel", help="Release channel"), 300 | note: str = typer.Option("", "--note", help="Release note or description"), 301 | ): 302 | """ 303 | Publishes an update to a specified release channel with optional notes. 304 | """ 305 | pass 306 | 307 | 308 | @app.command() 309 | def check_version( 310 | local_path: str = typer.Argument(..., help="Local path to check"), 311 | remote_url: str = typer.Option("", "--remote", help="Remote URL for comparison"), 312 | detailed: bool = typer.Option( 313 | False, "--detailed", help="Show detailed version info" 314 | ), 315 | ): 316 | """ 317 | Checks the version of a local path against a remote source, optionally showing details. 318 | """ 319 | pass 320 | 321 | 322 | @app.command() 323 | def queue_task( 324 | task_name: str = typer.Argument(..., help="Name of the task to queue"), 325 | priority: int = typer.Option(1, "--priority", help="Priority of the task"), 326 | delay: int = typer.Option( 327 | 0, "--delay", help="Delay in seconds before starting task" 328 | ), 329 | ): 330 | """ 331 | Queues a task with a specified priority and optional delay. 332 | """ 333 | pass 334 | 335 | 336 | @app.command() 337 | def remove_task( 338 | task_id: str = typer.Argument(..., help="ID of the task to remove"), 339 | force: bool = typer.Option(False, "--force", help="Remove without confirmation"), 340 | ): 341 | """ 342 | Removes a queued task by ID, optionally forcing removal without confirmation. 343 | """ 344 | pass 345 | 346 | 347 | @app.command() 348 | def list_tasks( 349 | show_all: bool = typer.Option( 350 | False, "--all", help="Show all tasks, including completed" 351 | ), 352 | sort_by: str = typer.Option( 353 | "priority", "--sort-by", help="Sort tasks by this field" 354 | ), 355 | ): 356 | """ 357 | Lists tasks, optionally including completed tasks or sorting by a different field. 358 | """ 359 | pass 360 | 361 | 362 | @app.command() 363 | def inspect_task( 364 | task_id: str = typer.Argument(..., help="ID of the task to inspect"), 365 | json_output: bool = typer.Option( 366 | False, "--json", help="Show output in JSON format" 367 | ), 368 | ): 369 | """ 370 | Inspects a specific task by ID, optionally in JSON format. 371 | """ 372 | pass 373 | -------------------------------------------------------------------------------- /.template.aider.conf.yml: -------------------------------------------------------------------------------- 1 | ########################################################## 2 | # Sample .aider.conf.yml 3 | # This file lists *all* the valid configuration entries. 4 | # Place in your home dir, or at the root of your git repo. 5 | ########################################################## 6 | 7 | # Note: You can only put OpenAI and Anthropic API keys in the yaml 8 | # config file. Keys for all APIs can be stored in a .env file 9 | # https://aider.chat/docs/config/dotenv.html 10 | 11 | ########## 12 | # options: 13 | 14 | ## show this help message and exit 15 | #help: xxx 16 | 17 | ####### 18 | # Main: 19 | 20 | ## Specify the OpenAI API key 21 | #openai-api-key: xxx 22 | 23 | ## Specify the Anthropic API key 24 | #anthropic-api-key: xxx 25 | 26 | ## Specify the model to use for the main chat 27 | #model: xxx 28 | 29 | ## Use claude-3-opus-20240229 model for the main chat 30 | #opus: false 31 | 32 | ## Use claude-3-5-sonnet-20241022 model for the main chat 33 | sonnet: true 34 | 35 | ## Use claude-3-5-haiku-20241022 model for the main chat 36 | #haiku: false 37 | 38 | ## Use gpt-4-0613 model for the main chat 39 | #4: false 40 | 41 | ## Use gpt-4o-2024-08-06 model for the main chat 42 | #4o: false 43 | 44 | ## Use gpt-4o-mini model for the main chat 45 | #mini: false 46 | 47 | ## Use gpt-4-1106-preview model for the main chat 48 | #4-turbo: false 49 | 50 | ## Use gpt-3.5-turbo model for the main chat 51 | #35turbo: false 52 | 53 | ## Use deepseek/deepseek-coder model for the main chat 54 | #deepseek: false 55 | 56 | ## Use o1-mini model for the main chat 57 | #o1-mini: false 58 | 59 | ## Use o1-preview model for the main chat 60 | #o1-preview: false 61 | 62 | ################# 63 | # Model Settings: 64 | 65 | ## List known models which match the (partial) MODEL name 66 | #list-models: xxx 67 | 68 | ## Specify the api base url 69 | #openai-api-base: xxx 70 | 71 | ## Specify the api_type 72 | #openai-api-type: xxx 73 | 74 | ## Specify the api_version 75 | #openai-api-version: xxx 76 | 77 | ## Specify the deployment_id 78 | #openai-api-deployment-id: xxx 79 | 80 | ## Specify the OpenAI organization ID 81 | #openai-organization-id: xxx 82 | 83 | ## Specify a file with aider model settings for unknown models 84 | #model-settings-file: .aider.model.settings.yml 85 | 86 | ## Specify a file with context window and costs for unknown models 87 | #model-metadata-file: .aider.model.metadata.json 88 | 89 | ## Add a model alias (can be used multiple times) 90 | #alias: xxx 91 | ## Specify multiple values like this: 92 | #alias: 93 | # - xxx 94 | # - yyy 95 | # - zzz 96 | 97 | ## Verify the SSL cert when connecting to models (default: True) 98 | #verify-ssl: true 99 | 100 | ## Specify what edit format the LLM should use (default depends on model) 101 | #edit-format: xxx 102 | 103 | ## Use architect edit format for the main chat 104 | #architect: false 105 | 106 | ## Specify the model to use for commit messages and chat history summarization (default depends on --model) 107 | #weak-model: xxx 108 | 109 | ## Specify the model to use for editor tasks (default depends on --model) 110 | #editor-model: xxx 111 | 112 | ## Specify the edit format for the editor model (default: depends on editor model) 113 | #editor-edit-format: xxx 114 | 115 | ## Only work with models that have meta-data available (default: True) 116 | #show-model-warnings: true 117 | 118 | ## Soft limit on tokens for chat history, after which summarization begins. If unspecified, defaults to the model's max_chat_history_tokens. 119 | #max-chat-history-tokens: xxx 120 | 121 | ## Specify the .env file to load (default: .env in git root) 122 | #env-file: .env 123 | 124 | ################# 125 | # Cache Settings: 126 | 127 | ## Enable caching of prompts (default: False) 128 | #cache-prompts: false 129 | 130 | ## Number of times to ping at 5min intervals to keep prompt cache warm (default: 0) 131 | #cache-keepalive-pings: false 132 | 133 | ################### 134 | # Repomap Settings: 135 | 136 | ## Suggested number of tokens to use for repo map, use 0 to disable (default: 1024) 137 | #map-tokens: xxx 138 | 139 | ## Control how often the repo map is refreshed. Options: auto, always, files, manual (default: auto) 140 | #map-refresh: auto 141 | 142 | ## Multiplier for map tokens when no files are specified (default: 2) 143 | #map-multiplier-no-files: true 144 | 145 | ################ 146 | # History Files: 147 | 148 | ## Specify the chat input history file (default: .aider.input.history) 149 | #input-history-file: .aider.input.history 150 | 151 | ## Specify the chat history file (default: .aider.chat.history.md) 152 | #chat-history-file: .aider.chat.history.md 153 | 154 | ## Restore the previous chat history messages (default: False) 155 | #restore-chat-history: false 156 | 157 | ## Log the conversation with the LLM to this file (for example, .aider.llm.history) 158 | #llm-history-file: xxx 159 | 160 | ################## 161 | # Output Settings: 162 | 163 | ## Use colors suitable for a dark terminal background (default: False) 164 | #dark-mode: false 165 | 166 | ## Use colors suitable for a light terminal background (default: False) 167 | #light-mode: false 168 | 169 | ## Enable/disable pretty, colorized output (default: True) 170 | #pretty: true 171 | 172 | ## Enable/disable streaming responses (default: True) 173 | #stream: true 174 | 175 | ## Set the color for user input (default: #00cc00) 176 | #user-input-color: #00cc00 177 | 178 | ## Set the color for tool output (default: None) 179 | #tool-output-color: xxx 180 | 181 | ## Set the color for tool error messages (default: #FF2222) 182 | #tool-error-color: #FF2222 183 | 184 | ## Set the color for tool warning messages (default: #FFA500) 185 | #tool-warning-color: #FFA500 186 | 187 | ## Set the color for assistant output (default: #0088ff) 188 | #assistant-output-color: #0088ff 189 | 190 | ## Set the color for the completion menu (default: terminal's default text color) 191 | #completion-menu-color: xxx 192 | 193 | ## Set the background color for the completion menu (default: terminal's default background color) 194 | #completion-menu-bg-color: xxx 195 | 196 | ## Set the color for the current item in the completion menu (default: terminal's default background color) 197 | #completion-menu-current-color: xxx 198 | 199 | ## Set the background color for the current item in the completion menu (default: terminal's default text color) 200 | #completion-menu-current-bg-color: xxx 201 | 202 | ## Set the markdown code theme (default: default, other options include monokai, solarized-dark, solarized-light) 203 | #code-theme: default 204 | 205 | ## Show diffs when committing changes (default: False) 206 | #show-diffs: false 207 | 208 | ############### 209 | # Git Settings: 210 | 211 | ## Enable/disable looking for a git repo (default: True) 212 | #git: true 213 | 214 | ## Enable/disable adding .aider* to .gitignore (default: True) 215 | #gitignore: true 216 | 217 | ## Specify the aider ignore file (default: .aiderignore in git root) 218 | #aiderignore: .aiderignore 219 | 220 | ## Only consider files in the current subtree of the git repository 221 | #subtree-only: false 222 | 223 | ## Enable/disable auto commit of LLM changes (default: True) 224 | auto-commits: false 225 | 226 | ## Enable/disable commits when repo is found dirty (default: True) 227 | #dirty-commits: true 228 | 229 | ## Attribute aider code changes in the git author name (default: True) 230 | #attribute-author: true 231 | 232 | ## Attribute aider commits in the git committer name (default: True) 233 | #attribute-committer: true 234 | 235 | ## Prefix commit messages with 'aider: ' if aider authored the changes (default: False) 236 | #attribute-commit-message-author: false 237 | 238 | ## Prefix all commit messages with 'aider: ' (default: False) 239 | #attribute-commit-message-committer: false 240 | 241 | ## Commit all pending changes with a suitable commit message, then exit 242 | #commit: false 243 | 244 | ## Specify a custom prompt for generating commit messages 245 | #commit-prompt: xxx 246 | 247 | ## Perform a dry run without modifying files (default: False) 248 | #dry-run: false 249 | 250 | ## Skip the sanity check for the git repository (default: False) 251 | #skip-sanity-check-repo: false 252 | 253 | ######################## 254 | # Fixing and committing: 255 | 256 | ## Lint and fix provided files, or dirty files if none provided 257 | #lint: false 258 | 259 | ## Specify lint commands to run for different languages, eg: "python: flake8 --select=..." (can be used multiple times) 260 | #lint-cmd: xxx 261 | ## Specify multiple values like this: 262 | #lint-cmd: 263 | # - xxx 264 | # - yyy 265 | # - zzz 266 | 267 | ## Enable/disable automatic linting after changes (default: True) 268 | #auto-lint: true 269 | 270 | ## Specify command to run tests 271 | #test-cmd: xxx 272 | 273 | ## Enable/disable automatic testing after changes (default: False) 274 | #auto-test: false 275 | 276 | ## Run tests and fix problems found 277 | #test: false 278 | 279 | ############ 280 | # Analytics: 281 | 282 | ## Enable/disable analytics for current session (default: random) 283 | #analytics: xxx 284 | 285 | ## Specify a file to log analytics events 286 | #analytics-log: xxx 287 | 288 | ## Permanently disable analytics 289 | #analytics-disable: false 290 | 291 | ################# 292 | # Other Settings: 293 | 294 | ## specify a file to edit (can be used multiple times) 295 | #file: xxx 296 | ## Specify multiple values like this: 297 | #file: 298 | # - xxx 299 | # - yyy 300 | # - zzz 301 | 302 | ## specify a read-only file (can be used multiple times) 303 | #read: xxx 304 | ## Specify multiple values like this: 305 | #read: 306 | # - xxx 307 | # - yyy 308 | # - zzz 309 | 310 | ## Use VI editing mode in the terminal (default: False) 311 | #vim: false 312 | 313 | ## Specify the language to use in the chat (default: None, uses system settings) 314 | #chat-language: xxx 315 | 316 | ## Show the version number and exit 317 | #version: xxx 318 | 319 | ## Check for updates and return status in the exit code 320 | #just-check-update: false 321 | 322 | ## Check for new aider versions on launch 323 | #check-update: true 324 | 325 | ## Show release notes on first run of new version (default: None, ask user) 326 | #show-release-notes: xxx 327 | 328 | ## Install the latest version from the main branch 329 | #install-main-branch: false 330 | 331 | ## Upgrade aider to the latest version from PyPI 332 | #upgrade: false 333 | 334 | ## Apply the changes from the given file instead of running the chat (debug) 335 | #apply: xxx 336 | 337 | ## Apply clipboard contents as edits using the main model's editor format 338 | #apply-clipboard-edits: false 339 | 340 | ## Always say yes to every confirmation 341 | #yes-always: false 342 | 343 | ## Enable verbose output 344 | #verbose: false 345 | 346 | ## Print the repo map and exit (debug) 347 | #show-repo-map: false 348 | 349 | ## Print the system prompts and exit (debug) 350 | #show-prompts: false 351 | 352 | ## Do all startup activities then exit before accepting user input (debug) 353 | #exit: false 354 | 355 | ## Specify a single message to send the LLM, process reply then exit (disables chat mode) 356 | #message: xxx 357 | 358 | ## Specify a file containing the message to send the LLM, process reply, then exit (disables chat mode) 359 | #message-file: xxx 360 | 361 | ## Load and execute /commands from a file on launch 362 | #load: xxx 363 | 364 | ## Specify the encoding for input and output (default: utf-8) 365 | #encoding: utf-8 366 | 367 | ## Specify the config file (default: search for .aider.conf.yml in git root, cwd or home directory) 368 | #config: xxx 369 | 370 | ## Run aider in your browser (default: False) 371 | #gui: false 372 | 373 | ## Enable/disable suggesting shell commands (default: True) 374 | suggest-shell-commands: false 375 | 376 | ## Enable/disable fancy input with history and completion (default: True) 377 | #fancy-input: true 378 | 379 | ## Enable/disable detection and offering to add URLs to chat (default: True) 380 | detect-urls: false 381 | 382 | ## Specify which editor to use for the /editor command 383 | editor: code 384 | 385 | ################# 386 | # Voice Settings: 387 | 388 | ## Audio format for voice recording (default: wav). webm and mp3 require ffmpeg 389 | #voice-format: wav 390 | 391 | ## Specify the language for voice using ISO 639-1 code (default: auto) 392 | #voice-language: en 393 | -------------------------------------------------------------------------------- /commands/template.py: -------------------------------------------------------------------------------- 1 | import typer 2 | from typing import Optional 3 | import sqlite3 4 | import os 5 | import json 6 | import csv 7 | import difflib 8 | import random 9 | import string 10 | import shutil 11 | from datetime import datetime 12 | 13 | import yaml 14 | 15 | app = typer.Typer() 16 | 17 | # ----------------------------------------------------- 18 | # Database helpers: create/connect/seed 19 | # ----------------------------------------------------- 20 | DB_NAME = "app_data.db" 21 | 22 | 23 | def get_connection(): 24 | """Return a connection to the SQLite database.""" 25 | return sqlite3.connect(DB_NAME) 26 | 27 | 28 | def create_db_if_not_exists(): 29 | """Create tables if they do not exist and seed them with mock data.""" 30 | conn = get_connection() 31 | cur = conn.cursor() 32 | 33 | # Create a sample 'users' table 34 | cur.execute( 35 | """ 36 | CREATE TABLE IF NOT EXISTS users ( 37 | id INTEGER PRIMARY KEY AUTOINCREMENT, 38 | username TEXT NOT NULL, 39 | role TEXT NOT NULL, 40 | created_at TEXT NOT NULL 41 | ) 42 | """ 43 | ) 44 | 45 | # Create a sample 'tasks' table 46 | cur.execute( 47 | """ 48 | CREATE TABLE IF NOT EXISTS tasks ( 49 | id INTEGER PRIMARY KEY AUTOINCREMENT, 50 | task_name TEXT NOT NULL, 51 | priority INTEGER NOT NULL, 52 | status TEXT NOT NULL, 53 | created_at TEXT NOT NULL 54 | ) 55 | """ 56 | ) 57 | 58 | # Create a sample 'logs' table 59 | cur.execute( 60 | """ 61 | CREATE TABLE IF NOT EXISTS logs ( 62 | id INTEGER PRIMARY KEY AUTOINCREMENT, 63 | message TEXT NOT NULL, 64 | level TEXT NOT NULL, 65 | created_at TEXT NOT NULL 66 | ) 67 | """ 68 | ) 69 | 70 | # Check if 'users' table has data; if not, seed 25 rows 71 | cur.execute("SELECT COUNT(*) FROM users") 72 | user_count = cur.fetchone()[0] 73 | if user_count == 0: 74 | roles = ["guest", "admin", "editor", "viewer"] 75 | for i in range(25): 76 | username = f"user_{i}" 77 | role = random.choice(roles) 78 | created_at = datetime.now().isoformat() 79 | cur.execute( 80 | "INSERT INTO users (username, role, created_at) VALUES (?, ?, ?)", 81 | (username, role, created_at), 82 | ) 83 | 84 | # Seed 'tasks' table 85 | cur.execute("SELECT COUNT(*) FROM tasks") 86 | task_count = cur.fetchone()[0] 87 | if task_count == 0: 88 | statuses = ["pending", "in-progress", "complete"] 89 | for i in range(25): 90 | task_name = f"task_{i}" 91 | priority = random.randint(1, 5) 92 | status = random.choice(statuses) 93 | created_at = datetime.now().isoformat() 94 | cur.execute( 95 | "INSERT INTO tasks (task_name, priority, status, created_at) VALUES (?, ?, ?, ?)", 96 | (task_name, priority, status, created_at), 97 | ) 98 | 99 | # Seed 'logs' table 100 | cur.execute("SELECT COUNT(*) FROM logs") 101 | logs_count = cur.fetchone()[0] 102 | if logs_count == 0: 103 | levels = ["INFO", "WARN", "ERROR", "DEBUG"] 104 | for i in range(25): 105 | message = f"Log entry number {i}" 106 | level = random.choice(levels) 107 | created_at = datetime.now().isoformat() 108 | cur.execute( 109 | "INSERT INTO logs (message, level, created_at) VALUES (?, ?, ?)", 110 | (message, level, created_at), 111 | ) 112 | 113 | conn.commit() 114 | conn.close() 115 | 116 | 117 | # Ensure the database and tables exist before we do anything 118 | create_db_if_not_exists() 119 | 120 | 121 | # ----------------------------------------------------- 122 | # Simple Caesar cipher for “encryption/decryption” demo 123 | # ----------------------------------------------------- 124 | def caesar_cipher_encrypt(plaintext: str, shift: int = 3) -> str: 125 | """A simple Caesar cipher encryption function.""" 126 | result = [] 127 | for ch in plaintext: 128 | if ch.isalpha(): 129 | start = ord("A") if ch.isupper() else ord("a") 130 | offset = (ord(ch) - start + shift) % 26 131 | result.append(chr(start + offset)) 132 | else: 133 | result.append(ch) 134 | return "".join(result) 135 | 136 | 137 | def caesar_cipher_decrypt(ciphertext: str, shift: int = 3) -> str: 138 | """A simple Caesar cipher decryption function.""" 139 | return caesar_cipher_encrypt(ciphertext, -shift) 140 | 141 | 142 | # ----------------------------------------------------- 143 | # 1) ping_server 144 | # ----------------------------------------------------- 145 | @app.command() 146 | def ping_server( 147 | wait: bool = typer.Option(False, "--wait", help="Wait for server response?") 148 | ): 149 | """ 150 | Pings the server, optionally waiting for a response. 151 | """ 152 | # Mock a server response time 153 | response_time_ms = random.randint(50, 300) 154 | result = f"Server pinged. Response time: {response_time_ms} ms." 155 | if wait: 156 | result += " (Waited for a response.)" 157 | typer.echo(result) 158 | return result 159 | 160 | 161 | # ----------------------------------------------------- 162 | # 2) show_config 163 | # ----------------------------------------------------- 164 | @app.command() 165 | def show_config( 166 | verbose: bool = typer.Option(False, "--verbose", help="Show config in detail?") 167 | ): 168 | """ 169 | Shows the current configuration from modules/assistant_config.py. 170 | """ 171 | try: 172 | 173 | config = "" 174 | 175 | with open("./assistant_config.yml", "r") as f: 176 | config = f.read() 177 | 178 | if verbose: 179 | result = f"Verbose config:\n{json.dumps(yaml.safe_load(config), indent=2)}" 180 | else: 181 | result = f"Config: {config}" 182 | typer.echo(result) 183 | return result 184 | except ImportError: 185 | result = "Error: Could not load assistant_config module" 186 | typer.echo(result) 187 | return result 188 | 189 | 190 | # ----------------------------------------------------- 191 | # 3) list_files 192 | # ----------------------------------------------------- 193 | @app.command() 194 | def list_files( 195 | path: str = typer.Argument(..., help="Path to list files from"), 196 | all_files: bool = typer.Option(False, "--all", help="Include hidden files"), 197 | ): 198 | """ 199 | Lists files in a directory. Optionally show hidden files. 200 | """ 201 | if not os.path.isdir(path): 202 | msg = f"Path '{path}' is not a valid directory." 203 | typer.echo(msg) 204 | return msg 205 | 206 | entries = os.listdir(path) 207 | if not all_files: 208 | entries = [e for e in entries if not e.startswith(".")] 209 | 210 | result = f"Files in '{path}': {entries}" 211 | typer.echo(result) 212 | return result 213 | 214 | 215 | # ----------------------------------------------------- 216 | # 3.5) list_users 217 | # ----------------------------------------------------- 218 | @app.command() 219 | def list_users( 220 | role: str = typer.Option(None, "--role", help="Filter users by role"), 221 | sort: str = typer.Option( 222 | "username", "--sort", help="Sort by field (username, role, created_at)" 223 | ), 224 | ): 225 | """ 226 | Lists all users, optionally filtered by role and sorted by specified field. 227 | """ 228 | conn = get_connection() 229 | cur = conn.cursor() 230 | 231 | query = "SELECT username, role, created_at FROM users" 232 | params = [] 233 | 234 | if role: 235 | query += " WHERE role = ?" 236 | params.append(role) 237 | 238 | query += f" ORDER BY {sort}" 239 | 240 | cur.execute(query, params) 241 | users = cur.fetchall() 242 | conn.close() 243 | 244 | if not users: 245 | result = "No users found." 246 | typer.echo(result) 247 | return result 248 | 249 | # Format output 250 | result = "Users:\n" 251 | for user in users: 252 | result += f"- {user[0]} (Role: {user[1]}, Created: {user[2]})\n" 253 | 254 | typer.echo(result) 255 | return result 256 | 257 | 258 | # ----------------------------------------------------- 259 | # 4) create_user 260 | # ----------------------------------------------------- 261 | @app.command() 262 | def create_user( 263 | username: str = typer.Argument(..., help="Name of the new user"), 264 | role: str = typer.Option("guest", "--role", help="Role for the new user"), 265 | ): 266 | """ 267 | Creates a new user with an optional role. 268 | """ 269 | conn = get_connection() 270 | cur = conn.cursor() 271 | now = datetime.now().isoformat() 272 | cur.execute( 273 | "INSERT INTO users (username, role, created_at) VALUES (?, ?, ?)", 274 | (username, role, now), 275 | ) 276 | conn.commit() 277 | conn.close() 278 | result = f"User '{username}' created with role '{role}'." 279 | typer.echo(result) 280 | return result 281 | 282 | 283 | # ----------------------------------------------------- 284 | # 5) delete_user 285 | # ----------------------------------------------------- 286 | @app.command() 287 | def delete_user( 288 | user_id: str = typer.Argument(..., help="ID of user to delete"), 289 | confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation prompt"), 290 | ): 291 | """ 292 | Deletes a user by ID. 293 | """ 294 | if not confirm: 295 | # In a real scenario, you'd prompt or handle differently 296 | typer.echo(f"Confirmation needed to delete user {user_id}. Use --confirm.") 297 | return f"Deletion of user {user_id} not confirmed." 298 | 299 | conn = get_connection() 300 | cur = conn.cursor() 301 | cur.execute("DELETE FROM users WHERE id = ?", (user_id,)) 302 | conn.commit() 303 | changes = cur.rowcount 304 | conn.close() 305 | 306 | if changes > 0: 307 | msg = f"User with ID {user_id} deleted." 308 | else: 309 | msg = f"No user found with ID {user_id}." 310 | typer.echo(msg) 311 | return msg 312 | 313 | 314 | # ----------------------------------------------------- 315 | # 6) generate_report 316 | # ----------------------------------------------------- 317 | @app.command() 318 | def generate_report( 319 | table_name: str = typer.Argument(..., help="Name of table to generate report from"), 320 | output_file: str = typer.Option("report.json", "--output", help="Output file name"), 321 | ): 322 | """ 323 | Generates a report from an existing database table and saves it to a file. 324 | """ 325 | conn = get_connection() 326 | cur = conn.cursor() 327 | 328 | # Get all data from the specified table 329 | cur.execute(f"SELECT * FROM {table_name}") 330 | rows = cur.fetchall() 331 | 332 | # Get column names from cursor description 333 | columns = [description[0] for description in cur.description] 334 | 335 | # Convert rows to list of dicts with column names 336 | data = [] 337 | for row in rows: 338 | data.append(dict(zip(columns, row))) 339 | 340 | report_data = { 341 | "table": table_name, 342 | "timestamp": datetime.now().isoformat(), 343 | "columns": columns, 344 | "row_count": len(rows), 345 | "data": data, 346 | } 347 | 348 | with open(output_file, "w") as f: 349 | json.dump(report_data, f, indent=2) 350 | 351 | conn.close() 352 | 353 | result = f"Report for table '{table_name}' generated and saved to {output_file}." 354 | typer.echo(result) 355 | typer.echo(json.dumps(report_data, indent=2)) 356 | return report_data 357 | 358 | 359 | # ----------------------------------------------------- 360 | # 7) backup_data 361 | # ----------------------------------------------------- 362 | @app.command() 363 | def backup_data( 364 | directory: str = typer.Argument(..., help="Directory to store backups"), 365 | full: bool = typer.Option(False, "--full", help="Perform a full backup"), 366 | ): 367 | """ 368 | Back up data to a specified directory, optionally performing a full backup. 369 | """ 370 | if not os.path.isdir(directory): 371 | os.makedirs(directory) 372 | 373 | backup_file = os.path.join( 374 | directory, f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db" 375 | ) 376 | shutil.copy(DB_NAME, backup_file) 377 | 378 | result = ( 379 | f"{'Full' if full else 'Partial'} backup completed. Saved to {backup_file}." 380 | ) 381 | typer.echo(result) 382 | return result 383 | 384 | 385 | # ----------------------------------------------------- 386 | # 8) restore_data 387 | # ----------------------------------------------------- 388 | @app.command() 389 | def restore_data( 390 | file_path: str = typer.Argument(..., help="File path of backup to restore"), 391 | overwrite: bool = typer.Option( 392 | False, "--overwrite", help="Overwrite existing data" 393 | ), 394 | ): 395 | """ 396 | Restores data from a backup file. 397 | """ 398 | if not os.path.isfile(file_path): 399 | msg = f"Backup file {file_path} does not exist." 400 | typer.echo(msg) 401 | return msg 402 | 403 | if not overwrite: 404 | msg = "Overwrite not confirmed. Use --overwrite to proceed." 405 | typer.echo(msg) 406 | return msg 407 | 408 | shutil.copy(file_path, DB_NAME) 409 | msg = f"Data restored from {file_path} to {DB_NAME}." 410 | typer.echo(msg) 411 | return msg 412 | 413 | 414 | # ----------------------------------------------------- 415 | # 9) summarize_logs 416 | # ----------------------------------------------------- 417 | @app.command() 418 | def summarize_logs( 419 | logs_path: str = typer.Argument(..., help="Path to log files"), 420 | lines: int = typer.Option(100, "--lines", help="Number of lines to summarize"), 421 | ): 422 | """ 423 | Summarizes log data from a specified path, limiting lines. 424 | """ 425 | if not os.path.isfile(logs_path): 426 | msg = f"Log file {logs_path} not found." 427 | typer.echo(msg) 428 | return msg 429 | 430 | with open(logs_path, "r") as f: 431 | all_lines = f.readlines() 432 | 433 | snippet = all_lines[:lines] 434 | result = f"Showing first {lines} lines from {logs_path}:\n" + "".join(snippet) 435 | typer.echo(result) 436 | return result 437 | 438 | 439 | # ----------------------------------------------------- 440 | # 10) upload_file 441 | # ----------------------------------------------------- 442 | @app.command() 443 | def upload_file( 444 | file_path: str = typer.Argument(..., help="Path of file to upload"), 445 | destination: str = typer.Option( 446 | "remote", "--destination", help="Destination label" 447 | ), 448 | secure: bool = typer.Option(True, "--secure", help="Use secure upload"), 449 | ): 450 | """ 451 | Uploads a file to a destination, optionally enforcing secure upload. 452 | """ 453 | if not os.path.isfile(file_path): 454 | msg = f"File {file_path} not found." 455 | typer.echo(msg) 456 | return msg 457 | 458 | # Mock upload 459 | result = f"File '{file_path}' uploaded to '{destination}' using {'secure' if secure else 'insecure'} mode." 460 | typer.echo(result) 461 | return result 462 | 463 | 464 | # ----------------------------------------------------- 465 | # 11) download_file 466 | # ----------------------------------------------------- 467 | @app.command() 468 | def download_file( 469 | url: str = typer.Argument(..., help="URL of file to download"), 470 | output_path: str = typer.Option(".", "--output", help="Local output path"), 471 | retry: int = typer.Option(3, "--retry", help="Number of times to retry"), 472 | ): 473 | """ 474 | Downloads a file from a URL with a specified number of retries. 475 | """ 476 | # In real scenario, you'd do requests, etc. We'll just mock it. 477 | filename = os.path.join(output_path, os.path.basename(url)) 478 | with open(filename, "w") as f: 479 | f.write("Downloaded data from " + url) 480 | 481 | result = f"File downloaded from {url} to {filename} with {retry} retries allowed." 482 | typer.echo(result) 483 | return result 484 | 485 | 486 | # ----------------------------------------------------- 487 | # 12) filter_records 488 | # ----------------------------------------------------- 489 | @app.command() 490 | def filter_records( 491 | source: str = typer.Argument(..., help="Data source to filter"), 492 | query: str = typer.Option("", "--query", help="Filtering query string"), 493 | limit: int = typer.Option(10, "--limit", help="Limit the number of results"), 494 | ): 495 | """ 496 | Filters records from a data source using a query, limiting the number of results. 497 | Example usage: filter_records table_name --query "admin" --limit 5 498 | """ 499 | conn = get_connection() 500 | cur = conn.cursor() 501 | 502 | # For demonstration, we'll assume the 'source' is a table name in the DB 503 | # and the 'query' is a substring to match against username or message, etc. 504 | # This is just a simple example. 505 | try: 506 | sql = f"SELECT * FROM {source} WHERE " 507 | if source == "users": 508 | sql += "username LIKE ?" 509 | elif source == "logs": 510 | sql += "message LIKE ?" 511 | elif source == "tasks": 512 | sql += "task_name LIKE ?" 513 | else: 514 | typer.echo(f"Unknown table: {source}") 515 | return f"Table '{source}' not recognized." 516 | 517 | sql += f" LIMIT {limit}" 518 | 519 | wildcard_query = f"%{query}%" 520 | cur.execute(sql, (wildcard_query,)) 521 | rows = cur.fetchall() 522 | 523 | result = ( 524 | f"Found {len(rows)} records in '{source}' with query '{query}'.\n{rows}" 525 | ) 526 | typer.echo(result) 527 | return result 528 | 529 | except sqlite3.OperationalError as e: 530 | msg = f"SQL error: {e}" 531 | typer.echo(msg) 532 | return msg 533 | finally: 534 | conn.close() 535 | 536 | 537 | # ----------------------------------------------------- 538 | # 16) compare_files 539 | # ----------------------------------------------------- 540 | @app.command() 541 | def compare_files( 542 | file_a: str = typer.Argument(..., help="First file to compare"), 543 | file_b: str = typer.Argument(..., help="Second file to compare"), 544 | diff_only: bool = typer.Option( 545 | False, "--diff-only", help="Show only the differences" 546 | ), 547 | ): 548 | """ 549 | Compares two files, optionally showing only differences. 550 | """ 551 | if not os.path.isfile(file_a) or not os.path.isfile(file_b): 552 | msg = f"One or both files do not exist: {file_a}, {file_b}" 553 | typer.echo(msg) 554 | return msg 555 | 556 | with open(file_a, "r") as fa, open(file_b, "r") as fb: 557 | lines_a = fa.readlines() 558 | lines_b = fb.readlines() 559 | 560 | diff = difflib.unified_diff(lines_a, lines_b, fromfile=file_a, tofile=file_b) 561 | 562 | if diff_only: 563 | # Show only differences 564 | differences = [] 565 | for line in diff: 566 | if line.startswith("+") or line.startswith("-"): 567 | differences.append(line) 568 | result = "\n".join(differences) 569 | else: 570 | # Show entire unified diff 571 | result = "".join(diff) 572 | 573 | typer.echo(result if result.strip() else "Files are identical.") 574 | return result 575 | 576 | 577 | # ----------------------------------------------------- 578 | # 17) encrypt_data 579 | # ----------------------------------------------------- 580 | @app.command() 581 | def encrypt_data( 582 | input_path: str = typer.Argument(..., help="Path of the file to encrypt"), 583 | output_path: str = typer.Option("encrypted.bin", "--output", help="Output file"), 584 | algorithm: str = typer.Option("AES", "--algorithm", help="Encryption algorithm"), 585 | ): 586 | """ 587 | Encrypts data using a specified algorithm (mocked by Caesar cipher here). 588 | """ 589 | if not os.path.isfile(input_path): 590 | msg = f"File {input_path} not found." 591 | typer.echo(msg) 592 | return msg 593 | 594 | with open(input_path, "r") as f: 595 | data = f.read() 596 | 597 | # We'll just mock the encryption using Caesar cipher 598 | encrypted = caesar_cipher_encrypt(data, 3) 599 | 600 | with open(output_path, "w") as f: 601 | f.write(encrypted) 602 | 603 | result = f"Data from {input_path} encrypted with {algorithm} (mock) and saved to {output_path}." 604 | typer.echo(result) 605 | return result 606 | 607 | 608 | # ----------------------------------------------------- 609 | # 18) decrypt_data 610 | # ----------------------------------------------------- 611 | @app.command() 612 | def decrypt_data( 613 | encrypted_file: str = typer.Argument(..., help="Path to encrypted file"), 614 | key: str = typer.Option(..., "--key", help="Decryption key"), 615 | output_path: str = typer.Option("decrypted.txt", "--output", help="Output file"), 616 | ): 617 | """ 618 | Decrypts an encrypted file using a key (ignored in this mock Caesar cipher). 619 | """ 620 | if not os.path.isfile(encrypted_file): 621 | msg = f"Encrypted file {encrypted_file} not found." 622 | typer.echo(msg) 623 | return msg 624 | 625 | with open(encrypted_file, "r") as f: 626 | encrypted_data = f.read() 627 | 628 | # Key is ignored in this Caesar cipher demo 629 | decrypted = caesar_cipher_decrypt(encrypted_data, 3) 630 | 631 | with open(output_path, "w") as f: 632 | f.write(decrypted) 633 | 634 | result = f"Data from {encrypted_file} decrypted and saved to {output_path}." 635 | typer.echo(result) 636 | return result 637 | 638 | 639 | # ----------------------------------------------------- 640 | # 21) migrate_database 641 | # ----------------------------------------------------- 642 | @app.command() 643 | def migrate_database( 644 | old_db: str = typer.Argument(..., help="Path to old database"), 645 | new_db: str = typer.Option(..., "--new-db", help="Path to new database"), 646 | dry_run: bool = typer.Option( 647 | False, "--dry-run", help="Perform a trial run without changing data" 648 | ), 649 | ): 650 | """ 651 | Migrates data from an old database to a new one, optionally doing a dry run. 652 | """ 653 | if not os.path.isfile(old_db): 654 | msg = f"Old database '{old_db}' not found." 655 | typer.echo(msg) 656 | return msg 657 | 658 | if dry_run: 659 | result = f"Dry run: would migrate {old_db} to {new_db}." 660 | typer.echo(result) 661 | return result 662 | 663 | shutil.copy(old_db, new_db) 664 | result = f"Database migrated from {old_db} to {new_db}." 665 | typer.echo(result) 666 | return result 667 | 668 | 669 | # ----------------------------------------------------- 670 | # 27) queue_task 671 | # ----------------------------------------------------- 672 | @app.command() 673 | def queue_task( 674 | task_name: str = typer.Argument(..., help="Name of the task to queue"), 675 | priority: int = typer.Option(1, "--priority", help="Priority of the task"), 676 | delay: int = typer.Option( 677 | 0, "--delay", help="Delay in seconds before starting task" 678 | ), 679 | ): 680 | """ 681 | Queues a task with a specified priority and optional delay. 682 | """ 683 | conn = get_connection() 684 | cur = conn.cursor() 685 | now = datetime.now().isoformat() 686 | cur.execute( 687 | "INSERT INTO tasks (task_name, priority, status, created_at) VALUES (?, ?, ?, ?)", 688 | (task_name, priority, "pending", now), 689 | ) 690 | conn.commit() 691 | task_id = cur.lastrowid 692 | conn.close() 693 | 694 | result = f"Task '{task_name}' queued with priority {priority}, delay {delay}s, assigned ID {task_id}." 695 | typer.echo(result) 696 | return result 697 | 698 | 699 | # ----------------------------------------------------- 700 | # 28) remove_task 701 | # ----------------------------------------------------- 702 | @app.command() 703 | def remove_task( 704 | task_id: str = typer.Argument(..., help="ID of the task to remove"), 705 | force: bool = typer.Option(False, "--force", help="Remove without confirmation"), 706 | ): 707 | """ 708 | Removes a queued task by ID, optionally forcing removal without confirmation. 709 | """ 710 | if not force: 711 | msg = f"Confirmation required to remove task {task_id}. Use --force." 712 | typer.echo(msg) 713 | return msg 714 | 715 | conn = get_connection() 716 | cur = conn.cursor() 717 | cur.execute("DELETE FROM tasks WHERE id = ?", (task_id,)) 718 | conn.commit() 719 | removed = cur.rowcount 720 | conn.close() 721 | 722 | if removed: 723 | msg = f"Task {task_id} removed." 724 | else: 725 | msg = f"Task {task_id} not found." 726 | typer.echo(msg) 727 | return msg 728 | 729 | 730 | # ----------------------------------------------------- 731 | # 29) list_tasks 732 | # ----------------------------------------------------- 733 | @app.command() 734 | def list_tasks( 735 | show_all: bool = typer.Option( 736 | False, "--all", help="Show all tasks, including completed" 737 | ), 738 | sort_by: str = typer.Option( 739 | "priority", "--sort-by", help="Sort tasks by this field" 740 | ), 741 | ): 742 | """ 743 | Lists tasks, optionally including completed tasks or sorting by a different field. 744 | """ 745 | valid_sort_fields = ["priority", "status", "created_at"] 746 | if sort_by not in valid_sort_fields: 747 | msg = f"Invalid sort field. Must be one of {valid_sort_fields}." 748 | typer.echo(msg) 749 | return msg 750 | 751 | conn = get_connection() 752 | cur = conn.cursor() 753 | if show_all: 754 | sql = f"SELECT id, task_name, priority, status, created_at FROM tasks ORDER BY {sort_by} ASC" 755 | else: 756 | sql = f"SELECT id, task_name, priority, status, created_at FROM tasks WHERE status != 'complete' ORDER BY {sort_by} ASC" 757 | 758 | cur.execute(sql) 759 | tasks = cur.fetchall() 760 | conn.close() 761 | 762 | result = "Tasks:\n" 763 | for t in tasks: 764 | result += ( 765 | f"ID={t[0]}, Name={t[1]}, Priority={t[2]}, Status={t[3]}, Created={t[4]}\n" 766 | ) 767 | 768 | typer.echo(result.strip()) 769 | return result 770 | 771 | 772 | # ----------------------------------------------------- 773 | # 30) inspect_task 774 | # ----------------------------------------------------- 775 | @app.command() 776 | def inspect_task( 777 | task_id: str = typer.Argument(..., help="ID of the task to inspect"), 778 | json_output: bool = typer.Option( 779 | False, "--json", help="Show output in JSON format" 780 | ), 781 | ): 782 | """ 783 | Inspects a specific task by ID, optionally in JSON format. 784 | """ 785 | conn = get_connection() 786 | cur = conn.cursor() 787 | cur.execute( 788 | "SELECT id, task_name, priority, status, created_at FROM tasks WHERE id = ?", 789 | (task_id,), 790 | ) 791 | row = cur.fetchone() 792 | conn.close() 793 | 794 | if not row: 795 | msg = f"No task found with ID {task_id}." 796 | typer.echo(msg) 797 | return msg 798 | 799 | task_dict = { 800 | "id": row[0], 801 | "task_name": row[1], 802 | "priority": row[2], 803 | "status": row[3], 804 | "created_at": row[4], 805 | } 806 | 807 | if json_output: 808 | result = json.dumps(task_dict, indent=2) 809 | else: 810 | result = f"Task ID={task_dict['id']}, Name={task_dict['task_name']}, Priority={task_dict['priority']}, Status={task_dict['status']}, Created={task_dict['created_at']}" 811 | typer.echo(result) 812 | return result 813 | 814 | 815 | # ----------------------------------------------------- 816 | # Entry point 817 | # ----------------------------------------------------- 818 | def main(): 819 | app() 820 | 821 | 822 | if __name__ == "__main__": 823 | main() 824 | --------------------------------------------------------------------------------