mirror of
https://github.com/sstent/AICycling_mcp.git
synced 2025-12-05 23:51:57 +00:00
274 lines
8.9 KiB
Python
274 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
CLI Interface - Simple command line interface for the cycling analyzer
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from pathlib import Path
|
|
import argparse
|
|
import sys
|
|
from rich.console import Console
|
|
from rich.markdown import Markdown
|
|
|
|
from ..config import Config, load_config, create_sample_config
|
|
from ..core.core_app import CyclingAnalyzerApp
|
|
from ..core.template_engine import create_default_templates
|
|
|
|
class CLI:
|
|
"""Command line interface"""
|
|
|
|
def __init__(self):
|
|
self.app = None
|
|
|
|
async def run(self, args):
|
|
"""Main CLI loop"""
|
|
# Setup configuration
|
|
try:
|
|
config = self._setup_config()
|
|
self.app = CyclingAnalyzerApp(config, test_mode=args.test)
|
|
|
|
# Setup logging
|
|
logging.basicConfig(level=getattr(logging, config.log_level.upper()))
|
|
|
|
# Initialize app
|
|
await self.app.initialize()
|
|
|
|
if args.command:
|
|
await self._handle_command(args)
|
|
else:
|
|
# Show initial status
|
|
await self._show_status()
|
|
# Main loop
|
|
await self._main_loop()
|
|
|
|
except KeyboardInterrupt:
|
|
print("\nGoodbye!")
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
logging.error(f"CLI error: {e}", exc_info=True)
|
|
finally:
|
|
if self.app:
|
|
await self.app.cleanup()
|
|
|
|
def _setup_config(self) -> Config:
|
|
"""Setup configuration and default files"""
|
|
# Create sample config if needed
|
|
create_sample_config()
|
|
|
|
# Load config
|
|
config = load_config()
|
|
|
|
# Validate required settings
|
|
if not config.openrouter_api_key or config.openrouter_api_key == "your_openrouter_api_key_here":
|
|
print("Please edit config.yaml with your OpenRouter API key")
|
|
print("Get your key from: https://openrouter.ai")
|
|
raise ValueError("OpenRouter API key not configured")
|
|
|
|
# Create default templates
|
|
create_default_templates(config.templates_dir)
|
|
|
|
return config
|
|
|
|
async def _show_status(self):
|
|
"""Show application status"""
|
|
print(f"\nStatus:")
|
|
print(f"- Available tools: {len(await self.app.list_available_tools())}")
|
|
print(f"- Available templates: {len(self.app.list_templates())}")
|
|
print(f"- Cached data keys: {list(self.app.get_cached_data().keys())}")
|
|
|
|
async def _main_loop(self):
|
|
"""Main interaction loop"""
|
|
while True:
|
|
print(f"\n{'='*60}")
|
|
print("CYCLING WORKOUT ANALYZER")
|
|
print(f"{'='*60}")
|
|
print("1. Analyze last cycling workout")
|
|
print("2. Get next workout suggestion")
|
|
print("3. Enhanced analysis")
|
|
print("4. List available MCP tools")
|
|
print("5. List available templates")
|
|
print("6. Show cached data")
|
|
print("7. Clear cache")
|
|
print("8. Exit")
|
|
print("-" * 60)
|
|
|
|
choice = input("Enter your choice (1-8): ").strip()
|
|
|
|
try:
|
|
if choice == "1":
|
|
await self._analyze_last_workout()
|
|
elif choice == "2":
|
|
await self._suggest_next_workout()
|
|
elif choice == "3":
|
|
await self._enhanced_analysis()
|
|
elif choice == "4":
|
|
await self._list_tools()
|
|
elif choice == "5":
|
|
self._list_templates()
|
|
elif choice == "6":
|
|
self._show_cached_data()
|
|
elif choice == "7":
|
|
self._clear_cache()
|
|
elif choice == "8":
|
|
break
|
|
else:
|
|
print("Invalid choice. Please try again.")
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
logging.error(f"Menu action error: {e}")
|
|
|
|
input("\nPress Enter to continue...")
|
|
|
|
async def _analyze_last_workout(self):
|
|
"""Analyze last workout"""
|
|
print("\nAnalyzing your last workout...")
|
|
|
|
# Load training rules
|
|
rules = self._load_training_rules()
|
|
|
|
result = await self.app.analyze_workout(
|
|
analysis_type="analyze_last_workout",
|
|
training_rules=rules
|
|
)
|
|
|
|
print("\n" + "="*50)
|
|
print("WORKOUT ANALYSIS")
|
|
print("="*50)
|
|
console = Console()
|
|
console.print(Markdown(result))
|
|
|
|
async def _suggest_next_workout(self):
|
|
"""Suggest next workout"""
|
|
print("\nGenerating workout suggestion...")
|
|
|
|
# Load training rules
|
|
rules = self._load_training_rules()
|
|
|
|
result = await self.app.suggest_next_workout(training_rules=rules)
|
|
|
|
print("\n" + "="*50)
|
|
print("NEXT WORKOUT SUGGESTION")
|
|
print("="*50)
|
|
console = Console()
|
|
console.print(Markdown(result))
|
|
|
|
async def _enhanced_analysis(self):
|
|
"""Enhanced analysis menu"""
|
|
print("\nSelect analysis type:")
|
|
print("a) FTP estimation")
|
|
print("b) Gear analysis")
|
|
print("c) Training load analysis")
|
|
|
|
choice = input("Enter choice (a-c): ").strip().lower()
|
|
|
|
analysis_types = {
|
|
'a': 'ftp_estimation',
|
|
'b': 'gear_analysis',
|
|
'c': 'training_load'
|
|
}
|
|
|
|
if choice not in analysis_types:
|
|
print("Invalid choice.")
|
|
return
|
|
|
|
analysis_type = analysis_types[choice]
|
|
print(f"\nPerforming {analysis_type.replace('_', ' ').title()} analysis...")
|
|
|
|
# Load training rules
|
|
rules = self._load_training_rules()
|
|
|
|
result = await self.app.enhanced_analysis(
|
|
analysis_type,
|
|
training_rules=rules
|
|
)
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f"ENHANCED {analysis_type.replace('_', ' ').upper()} ANALYSIS")
|
|
print("="*50)
|
|
console = Console()
|
|
console.print(Markdown(result))
|
|
|
|
async def _list_tools(self):
|
|
"""List available tools"""
|
|
tools = await self.app.list_available_tools()
|
|
if tools:
|
|
self.app.mcp_client.print_tools()
|
|
else:
|
|
print("No MCP tools available")
|
|
|
|
def _list_templates(self):
|
|
"""List available templates"""
|
|
templates = self.app.list_templates()
|
|
print(f"\nAvailable templates ({len(templates)}):")
|
|
for template in templates:
|
|
print(f" - {template}")
|
|
|
|
def _show_cached_data(self):
|
|
"""Show cached data"""
|
|
cached_data = self.app.get_cached_data()
|
|
print(f"\nCached data ({len(cached_data)} items):")
|
|
for key, value in cached_data.items():
|
|
data_type = type(value).__name__
|
|
if isinstance(value, (dict, list)):
|
|
size = len(value)
|
|
print(f" - {key}: {data_type} (size: {size})")
|
|
else:
|
|
print(f" - {key}: {data_type}")
|
|
|
|
def _clear_cache(self):
|
|
"""Clear cache"""
|
|
self.app.cache_manager.clear()
|
|
print("Cache cleared")
|
|
|
|
def _load_training_rules(self) -> str:
|
|
"""Load training rules from file"""
|
|
rules_file = Path(self.app.config.rules_file)
|
|
if rules_file.exists():
|
|
with open(rules_file, 'r') as f:
|
|
return f.read()
|
|
else:
|
|
# Create default rules
|
|
default_rules = """
|
|
Training Goals:
|
|
- Improve FTP (Functional Threshold Power)
|
|
- Build endurance for long rides
|
|
- Maintain consistent training
|
|
|
|
Power Zones (adjust based on your FTP):
|
|
- Zone 1 (Active Recovery): < 55% FTP
|
|
- Zone 2 (Endurance): 56-75% FTP
|
|
- Zone 3 (Tempo): 76-90% FTP
|
|
- Zone 4 (Lactate Threshold): 91-105% FTP
|
|
- Zone 5 (VO2 Max): 106-120% FTP
|
|
|
|
Weekly Structure:
|
|
- 70-80% easy/moderate intensity
|
|
- 20-30% high intensity
|
|
- At least 1 rest day per week
|
|
"""
|
|
rules_file.parent.mkdir(exist_ok=True)
|
|
with open(rules_file, 'w') as f:
|
|
f.write(default_rules)
|
|
return default_rules
|
|
|
|
async def _handle_command(self, args):
|
|
"""Handle non-interactive commands"""
|
|
if args.command == "analyze_last":
|
|
await self._analyze_last_workout()
|
|
else:
|
|
print(f"Unknown command: {args.command}")
|
|
|
|
async def main():
|
|
"""CLI entry point"""
|
|
parser = argparse.ArgumentParser(description="Cycling Workout Analyzer")
|
|
parser.add_argument('command', nargs='?', help="Command to execute (e.g., analyze_last)")
|
|
parser.add_argument('--test', action='store_true', help="Test mode: print rendered prompt without calling LLM")
|
|
args = parser.parse_args()
|
|
|
|
cli = CLI()
|
|
await cli.run(args)
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main()) |