devai-cli/src/devai/commands/prompts.py (336 lines of code) (raw):
import os
import yaml
import click
import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from vertexai.generative_models import GenerativeModel
from google.cloud.aiplatform import telemetry
from google.api_core.gapic_v1.client_info import ClientInfo
from .constants import USER_AGENT, MODEL_NAME
import pkg_resources
# User configuration file path
CONFIG_DIR = Path.home() / '.devai'
CONFIG_FILE = CONFIG_DIR / 'config.json'
def get_package_prompts_dir() -> Path:
"""Get the path to the package's prompts directory."""
# Get the package directory (where devai/commands/prompts.py is)
package_dir = Path(__file__).parent.parent.parent
# Go up one level to src
return package_dir / 'prompts'
def get_user_prompts_dir() -> Optional[Path]:
"""Get the user's custom prompts directory if configured."""
# First check if user has set a custom path in config
config = get_config()
prompts_dir = config.get('prompts_dir')
if prompts_dir and prompts_dir.strip():
return Path(prompts_dir)
# If no custom path, check if user has initialized prompts directory
default_dir = Path.home() / '.devai' / 'prompts'
if default_dir.exists():
return default_dir
return None
def get_prompts_dir() -> Path:
"""Get the prompts directory path from config or default."""
# Check for user's custom prompts directory first
user_dir = get_user_prompts_dir()
if user_dir:
return user_dir
# Default to package prompts directory
return get_package_prompts_dir()
def find_prompt_file(path: str) -> Tuple[Path, bool]:
"""Find a prompt file, checking user directory first, then package directory.
Returns (path, is_user_override)"""
# Check user's custom directory first
user_dir = get_user_prompts_dir()
if user_dir:
user_file = user_dir / path
if user_file.exists():
return user_file, True
# Fall back to package directory
package_file = get_package_prompts_dir() / path
return package_file, False
def get_config():
"""Get user configuration."""
if not CONFIG_FILE.exists():
return {}
try:
with open(CONFIG_FILE, 'r') as f:
return json.load(f)
except Exception:
return {}
def save_config(config):
"""Save user configuration."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
def set_prompts_dir(path: str):
"""Set the prompts directory path in config."""
config = get_config()
config['prompts_dir'] = str(path)
save_config(config)
return Path(path)
@click.group()
def prompts():
"""Manage DevAI prompt templates."""
pass
@prompts.command(name='config')
@click.option('--set-path', help='Set the prompts directory path')
@click.option('--show', is_flag=True, help='Show current prompts directory path')
@click.option('--reset', is_flag=True, help='Reset to use package prompts')
def config_prompts(set_path: Optional[str], show: bool, reset: bool):
"""Configure prompts directory settings."""
if reset:
config = get_config()
if 'prompts_dir' in config:
del config['prompts_dir']
save_config(config)
click.echo("Reset to use package prompts")
return
if show:
user_dir = get_user_prompts_dir()
if user_dir:
click.echo(f"Using custom prompts directory: {user_dir}")
else:
click.echo(f"Using package prompts directory: {get_package_prompts_dir()}")
return
if set_path:
try:
new_path = set_prompts_dir(set_path)
click.echo(f"Custom prompts directory set to: {new_path}")
click.echo("You can override package prompts by creating files with the same names in this directory")
except Exception as e:
click.echo(f"Error setting prompts directory: {str(e)}", err=True)
return
click.echo("Please specify either --set-path, --show, or --reset")
@prompts.command(name='list')
@click.option('--category', '-c', help='Filter prompts by category')
@click.option('--subcategory', '-s', help='Filter prompts by subcategory')
@click.option('--tag', '-t', multiple=True, help='Filter prompts by tags')
def list_prompts(category: Optional[str], subcategory: Optional[str], tag: Optional[List[str]]):
"""List available prompt templates."""
# Get all yaml files from both package and user directories
package_files = []
user_files = []
# Add package prompts
package_dir = get_package_prompts_dir()
if package_dir.exists():
package_files.extend(package_dir.rglob('*.yaml'))
# Add user prompts (these will override package prompts)
user_dir = get_user_prompts_dir()
if user_dir and user_dir.exists():
user_files.extend(user_dir.rglob('*.yaml'))
if not package_files and not user_files:
click.echo("No prompt templates found")
return
# Track which files we've shown to avoid duplicates
shown_files = set()
def print_prompt_info(prompt_file: Path, is_user_override: bool = False):
try:
with open(prompt_file, 'r') as f:
data = yaml.safe_load(f)
metadata = data.get('metadata', {})
# Apply filters
if category and metadata.get('category') != category:
return
if subcategory and metadata.get('subcategory') != subcategory:
return
if tag and not all(t in metadata.get('tags', []) for t in tag):
return
# Print prompt info
click.echo(f"\n{click.style(metadata.get('name', 'Unnamed'), fg='green')}")
click.echo(f" Category: {metadata.get('category', 'N/A')}")
click.echo(f" Subcategory: {metadata.get('subcategory', 'N/A')}")
click.echo(f" Description: {metadata.get('description', 'No description')}")
click.echo(f" Tags: {', '.join(metadata.get('tags', []))}")
if is_user_override:
click.echo(f" Path: {prompt_file.absolute()}")
click.echo(" (User override)")
else:
click.echo(f" Path: {prompt_file.relative_to(prompt_file.parent.parent)}")
shown_files.add(prompt_file.name)
except Exception as e:
click.echo(f"Error reading {prompt_file}: {str(e)}", err=True)
# First show package prompts (default)
if package_files:
click.echo("\n=== Default Prompts ===")
for prompt_file in package_files:
# Skip if this file is overridden by a user file
if prompt_file.name in [f.name for f in user_files]:
continue
print_prompt_info(prompt_file)
# Then show user prompts (custom)
if user_files:
click.echo("\n=== Custom Prompts ===")
for prompt_file in user_files:
print_prompt_info(prompt_file, is_user_override=True)
@prompts.command(name='show')
@click.argument('path')
def show_prompt(path: str):
"""Show details of a specific prompt template."""
prompt_file, is_user_override = find_prompt_file(path)
if not prompt_file.exists():
click.echo(f"Error: Prompt template not found at {path}", err=True)
return
try:
with open(prompt_file, 'r') as f:
data = yaml.safe_load(f)
# Print metadata
metadata = data.get('metadata', {})
click.echo(f"\n{click.style(metadata.get('name', 'Unnamed'), fg='green')}")
if is_user_override:
click.echo("(User override)")
click.echo(f"Category: {metadata.get('category', 'N/A')}")
click.echo(f"Subcategory: {metadata.get('subcategory', 'N/A')}")
click.echo(f"Description: {metadata.get('description', 'No description')}")
click.echo(f"Tags: {', '.join(metadata.get('tags', []))}")
# Print configuration
config = data.get('configuration', {})
click.echo("\nConfiguration:")
click.echo(f" Temperature: {config.get('temperature', 'N/A')}")
click.echo(f" Max Tokens: {config.get('max_tokens', 'N/A')}")
click.echo(f" Output Format: {config.get('output_format', 'N/A')}")
# Print prompt sections
prompt = data.get('prompt', {})
click.echo("\nSystem Context:")
click.echo(prompt.get('system_context', 'N/A'))
click.echo("\nInstruction:")
click.echo(prompt.get('instruction', 'N/A'))
if 'examples' in prompt:
click.echo("\nExamples:")
for i, example in enumerate(prompt['examples'], 1):
click.echo(f"\nExample {i}:")
click.echo("Input:")
click.echo(example.get('input', 'N/A'))
click.echo("Output:")
click.echo(example.get('output', 'N/A'))
# Print validation
validation = data.get('validation', {})
if validation:
click.echo("\nValidation:")
click.echo(f" Required Sections: {', '.join(validation.get('required_sections', []))}")
click.echo(f" Quality Checks: {', '.join(validation.get('quality_checks', []))}")
except Exception as e:
click.echo(f"Error reading prompt template: {str(e)}", err=True)
@prompts.command(name='create')
@click.option('--name', prompt='Template name')
@click.option('--category', prompt='Category')
@click.option('--subcategory', prompt='Subcategory')
@click.option('--description', prompt='Description')
@click.option('--tags', prompt='Tags (comma-separated)')
def create_prompt(name: str, category: str, subcategory: str, description: str, tags: str):
"""Create a new prompt template."""
# Always create in user's custom directory
user_dir = get_user_prompts_dir()
if not user_dir:
click.echo("Error: Please set a custom prompts directory first using 'devai prompts config --set-path'")
return
category_dir = user_dir / category.lower()
# Create category directory if it doesn't exist
category_dir.mkdir(parents=True, exist_ok=True)
# Create template file
template_file = category_dir / f"{subcategory.lower()}.yaml"
if template_file.exists():
if not click.confirm(f"Template already exists at {template_file}. Overwrite?"):
return
template = {
'metadata': {
'name': name,
'description': description,
'version': '1.0',
'category': category,
'subcategory': subcategory,
'author': 'DevAI',
'last_updated': '2024-03-25',
'tags': [tag.strip() for tag in tags.split(',')]
},
'configuration': {
'temperature': 0.7,
'max_tokens': 1024,
'output_format': 'markdown'
},
'prompt': {
'system_context': f"You are an expert in {category} with extensive experience in {subcategory}.",
'instruction': "### Task Description ###\n\n### Focus Areas ###\n\n### Analysis Requirements ###\n\n### Output Format ###\n",
'examples': [
{
'input': 'Example input',
'output': 'Example output'
}
]
},
'validation': {
'required_sections': [],
'output_schema': {},
'quality_checks': []
}
}
try:
with open(template_file, 'w') as f:
yaml.dump(template, f, default_flow_style=False)
click.echo(f"Created new template at {template_file}")
except Exception as e:
click.echo(f"Error creating template: {str(e)}", err=True)
@prompts.command(name='execute')
@click.argument('path')
@click.option('--input', '-i', help='Input text to send with the prompt')
@click.option('--output-format', '-f', type=click.Choice(['markdown', 'json', 'text']), help='Override the output format')
def execute_prompt(path: str, input: Optional[str], output_format: Optional[str]):
"""Execute a prompt template with Gemini."""
prompt_file, is_user_override = find_prompt_file(path)
if not prompt_file.exists():
click.echo(f"Error: Prompt template not found at {path}", err=True)
return
try:
# Load the prompt template
with open(prompt_file, 'r') as f:
template = yaml.safe_load(f)
# Get configuration
config = template.get('configuration', {})
if output_format:
config['output_format'] = output_format
# Get the prompt sections
prompt_data = template.get('prompt', {})
system_context = prompt_data.get('system_context', '')
instruction = prompt_data.get('instruction', '')
# Build the full prompt
full_prompt = f"{system_context}\n\n{instruction}"
if input:
full_prompt += f"\n\nInput:\n{input}"
# Initialize Gemini with telemetry
client_info = ClientInfo(user_agent=USER_AGENT)
with telemetry.tool_context_manager(USER_AGENT):
model = GenerativeModel(MODEL_NAME)
# Generate response
response = model.generate_content(
full_prompt,
generation_config={
'temperature': config.get('temperature', 0.7),
'max_output_tokens': config.get('max_tokens', 1024),
}
)
# Format and display the response
if config.get('output_format') == 'json':
try:
import json
# Try to parse the response as JSON if it looks like JSON
if response.text.strip().startswith('{') or response.text.strip().startswith('['):
json_response = json.loads(response.text)
click.echo(json.dumps(json_response, indent=2))
else:
click.echo(response.text)
except json.JSONDecodeError:
click.echo(response.text)
else:
click.echo(response.text)
except Exception as e:
click.echo(f"Error executing prompt: {str(e)}", err=True)
@prompts.command(name='init')
@click.option('--force', is_flag=True, help='Force initialization even if directory exists')
def init_prompts(force: bool):
"""Initialize a prompts directory with sample templates."""
# Get the prompts directory from config or default
prompts_dir = get_prompts_dir()
# Check if directory exists
if prompts_dir.exists():
if not force:
if not click.confirm(f"Directory {prompts_dir} already exists. Initialize anyway?"):
return
click.echo(f"Initializing existing directory: {prompts_dir}")
else:
click.echo(f"Creating new prompts directory: {prompts_dir}")
prompts_dir.mkdir(parents=True, exist_ok=True)
# Create basic directory structure
categories = ['security', 'testing', 'performance', 'documentation']
for category in categories:
(prompts_dir / category).mkdir(exist_ok=True)
# Create a sample template
sample_template = {
'metadata': {
'name': 'Web Security Review',
'description': 'Review web application security best practices',
'version': '1.0',
'category': 'security',
'subcategory': 'web-security',
'author': 'DevAI',
'last_updated': '2024-03-25',
'tags': ['security', 'web', 'review']
},
'configuration': {
'temperature': 0.7,
'max_tokens': 1024,
'output_format': 'markdown'
},
'prompt': {
'system_context': 'You are a security expert reviewing web applications.',
'instruction': 'Review the following web application code for security vulnerabilities and best practices.',
'examples': [
{
'input': 'Check this login form for security issues',
'output': '1. Missing CSRF token\n2. Password field lacks minimum length requirement\n3. No rate limiting on login attempts'
}
]
},
'validation': {
'required_sections': ['vulnerabilities', 'recommendations'],
'output_schema': {},
'quality_checks': []
}
}
# Save the sample template
sample_file = prompts_dir / 'security' / 'web-security.yaml'
with open(sample_file, 'w') as f:
yaml.dump(sample_template, f, default_flow_style=False)
click.echo(f"Initialized prompts directory")
click.echo(f"Created sample template: security/web-security.yaml")
click.echo("\nYou can now:")
click.echo("1. Add your own prompt templates to this directory")
click.echo("2. Override package prompts by creating files with the same names")
click.echo("3. Use 'devai prompts list' to see all available templates")