init commit
This commit is contained in:
183
app/main.py
Normal file
183
app/main.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""SneakyCode entrypoint — argument parsing, config loading, and interactive REPL."""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from app.agent.context import SessionContext
|
||||
from app.models.config import AppConfig, load_config
|
||||
from app.services.llm import LLMClient, LLMConnectionError, LLMError
|
||||
from app.services.streaming import StreamHandler
|
||||
from app.utils.display import (
|
||||
print_banner,
|
||||
print_error,
|
||||
print_history,
|
||||
print_info,
|
||||
print_success,
|
||||
print_token_usage,
|
||||
print_user_message,
|
||||
print_warning,
|
||||
)
|
||||
from app.utils.logging import console, get_logger, setup_logging
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse command-line arguments.
|
||||
|
||||
Returns:
|
||||
Parsed arguments namespace.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="sneakycode",
|
||||
description="SneakyCode — A privacy-first local AI coding agent",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Path to config YAML file (default: config/config.yaml)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Enable verbose (DEBUG) logging",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-file",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Path to log file for persistent logging",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def _run_repl(
|
||||
ctx: SessionContext,
|
||||
config: AppConfig,
|
||||
logger: structlog.stdlib.BoundLogger,
|
||||
) -> None:
|
||||
"""Run the interactive REPL loop with streaming LLM responses.
|
||||
|
||||
Args:
|
||||
ctx: Session context for conversation state.
|
||||
config: Application configuration.
|
||||
logger: Structured logger instance.
|
||||
"""
|
||||
async with LLMClient(config.llm) as client:
|
||||
handler = StreamHandler(config.display)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = console.input("[bold cyan]> [/bold cyan]")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
console.print("\n[dim]Goodbye![/dim]")
|
||||
break
|
||||
|
||||
user_input = user_input.strip()
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
# Handle slash commands
|
||||
if user_input.startswith("/"):
|
||||
command = user_input.lower()
|
||||
if command == "/quit":
|
||||
console.print("[dim]Goodbye![/dim]")
|
||||
break
|
||||
elif command == "/history":
|
||||
print_history(ctx.get_history())
|
||||
elif command == "/clear":
|
||||
ctx.clear_history()
|
||||
print_success("Conversation history cleared.")
|
||||
else:
|
||||
print_warning(f"Unknown command: {user_input}")
|
||||
continue
|
||||
|
||||
# Add user message and display it
|
||||
ctx.add_message("user", user_input)
|
||||
print_user_message(user_input)
|
||||
|
||||
# Stream LLM response
|
||||
try:
|
||||
chunk_iter = client.stream_chat(ctx.get_history())
|
||||
assistant_msg = await handler.process_stream(chunk_iter)
|
||||
except KeyboardInterrupt:
|
||||
print_warning("Response interrupted.")
|
||||
handler.reset()
|
||||
continue
|
||||
except LLMConnectionError as e:
|
||||
print_error(f"Connection error: {e}")
|
||||
continue
|
||||
except LLMError as e:
|
||||
print_error(f"LLM error: {e}")
|
||||
continue
|
||||
|
||||
# Handle empty response
|
||||
if assistant_msg.content is None and assistant_msg.tool_calls is None:
|
||||
print_warning("Received empty response from model.")
|
||||
|
||||
# Record assistant message in history
|
||||
ctx.add_message(
|
||||
"assistant",
|
||||
assistant_msg.content,
|
||||
tool_calls=assistant_msg.tool_calls,
|
||||
)
|
||||
|
||||
# Record API token usage if available, fall back to heuristic
|
||||
if handler.usage:
|
||||
ctx.token_counter.count_usage(handler.usage)
|
||||
|
||||
# Show token usage if configured
|
||||
if config.display.show_token_usage:
|
||||
print_token_usage(
|
||||
ctx.token_counter.cumulative_usage.total_tokens
|
||||
or ctx.estimated_tokens,
|
||||
ctx.token_counter.budget,
|
||||
)
|
||||
|
||||
handler.reset()
|
||||
logger.debug("message_exchanged", message_count=ctx.message_count)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main entrypoint: load config, setup logging, launch interactive REPL."""
|
||||
args = parse_args()
|
||||
|
||||
# Setup logging first
|
||||
setup_logging(
|
||||
log_file=args.log_file,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Load configuration
|
||||
try:
|
||||
config = load_config(config_path=args.config)
|
||||
except (FileNotFoundError, ValueError) as e:
|
||||
print_error(f"Configuration error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("config_loaded", model=config.llm.model, endpoint=config.llm.endpoint)
|
||||
|
||||
# Print startup info
|
||||
print_banner()
|
||||
print_info(f"Model: {config.llm.model}")
|
||||
print_info(f"Endpoint: {config.llm.endpoint}")
|
||||
print_info(f"Workspace: {config.agent.workspace_root}")
|
||||
|
||||
if args.verbose:
|
||||
print_info("Verbose mode enabled")
|
||||
|
||||
# Create session and start REPL
|
||||
ctx = SessionContext(config)
|
||||
logger.info("startup_complete")
|
||||
|
||||
print_info("Commands: /quit, /history, /clear")
|
||||
asyncio.run(_run_repl(ctx, config, logger))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user