diff --git a/cli/example/example.md b/cli/example/example.md new file mode 100644 index 00000000..af3ca2cf --- /dev/null +++ b/cli/example/example.md @@ -0,0 +1,101 @@ +# Potpie Project Usage Summary + +## Overview +Potpie is a CLI tool designed to manage and interact with repositories, perform codebase analysis, and communicate with AI-powered agents for various development tasks. Below is a summary of my experience using Potpie. + +## Installation +To install Potpie from the built distribution: +```sh +pip install cli/dist/potpie-0.0.1-py3-none-any.whl +``` +This installs all dependencies required for Potpie to function properly. + +## Starting the Server +After installation, the Potpie server can be started with: +```sh +potpie start +``` +### Logs: +- Environment set to **development** +- Docker containers started successfully +- PostgreSQL server detected and running +- Database migrations completed successfully +- Server started at `http://localhost:8001` + +## Listing Projects +To list all projects currently available in Potpie: +```sh +potpie projects +``` +### Output: +``` +╒══════╤════════╤══════════╕ +│ ID │ Name │ Status │ +╞══════╪════════╪══════════╡ +╘══════╧════════╧══════════╛ +``` + +## Parsing a Repository +Parsing a local repository with Potpie is straightforward: +```sh +potpie parse "/home/deepesh/Development/public/opensource/you-get" +``` +### Parsing Status Updates: +- `submitted` +- `parsed` +- `ready` + +Once completed, the project appears in the projects list: +``` +╒══════════════════════════════════════╤═════════╤══════════╕ +│ ID │ Name │ Status │ +╞══════════════════════════════════════╪═════════╪══════════╡ +│ 01950a23-27f0-7b7d-bcb9-5d6c65773253 │ you-get │ ready │ +╘══════════════════════════════════════╧═════════╧══════════╛ +``` + +## Starting a Conversation +To interact with the AI-powered agents: +```sh +potpie conversation create "My first conversation with youget" +``` +After selecting the project and agent, the conversation is successfully created. + +### Available Agents: +1. Codebase Q&A Agent +2. Debugging with Knowledge Graph Agent +3. Unit Test Agent +4. Integration Test Agent +5. Low-Level Design Agent +6. Code Changes Agent +7. Code Generation Agent + +To start messaging: +```sh +potpie conversation message +``` +Example interaction: +``` +You: hello, can you tell me about the codebase? and what it is doing? +Bot: The "you-get" project is a CLI utility for downloading media content from various websites... +``` + +## Stopping the Server +To stop the Potpie server and its associated services: +```sh +potpie stop +``` +### Logs: +- Terminated active processes +- Stopped Docker containers (Redis, PostgreSQL, Neo4j) +- All services stopped successfully + +## Summary +- Successfully installed and started Potpie +- Parsed and listed repositories +- Created and engaged in a conversation with an AI agent +- Explored the functionality of "you-get" through the conversation feature +- Stopped the Potpie server and services gracefully + +Potpie proves to be an efficient tool for managing codebases, analyzing repositories, and interacting with AI-driven development assistants. 🚀 + diff --git a/cli/src/potpie/main.py b/cli/src/potpie/main.py index 990374e9..0e35fe1a 100644 --- a/cli/src/potpie/main.py +++ b/cli/src/potpie/main.py @@ -30,6 +30,20 @@ api_wrapper = ApiWrapper() +def handle_error(func): + """Decorator for handling API errors""" + + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except requests.RequestException as e: + logging.error("Network error occurred: %s", e) + except Exception as e: + logging.error("An unexpected error occurred: %s", e) + + return wrapper + + def loading_animation(message: str): """Display loading animation for five seconds""" spinner = itertools.cycle(["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]) @@ -46,30 +60,27 @@ def cli(): """CLI tool for managing the potpie application""" +@handle_error @cli.command() def start(): """Start the server and all related services""" click.secho("Poitre server starting...", fg="blue", bold=True) - try: - signal.signal(signal.SIGINT, server_manager.handle_shutdown) - signal.signal(signal.SIGTERM, server_manager.handle_shutdown) - server_manager.start_server() - click.secho("Poitre server started successfully.", fg="green", bold=True) - except Exception as e: - logging.error("Error during startup: %s", e) + signal.signal(signal.SIGINT, server_manager.handle_shutdown) + signal.signal(signal.SIGTERM, server_manager.handle_shutdown) + server_manager.start_server() + click.secho("Poitre server started successfully.", fg="green", bold=True) +@handle_error @cli.command() -@handle_api_error def stop(): """Stop the server and all related services""" click.secho("Stopping Poitre server...", fg="blue", bold=True) - try: - server_manager.stop_server() - click.secho("Poitre server stopped successfully.", fg="green", bold=True) - except Exception as e: - logging.error("Error during shutdown: %s", e) + server_manager.stop_server() + click.secho("Poitre server stopped successfully.", fg="green", bold=True) + +@handle_error @cli.command() @click.argument("repo") @click.option("--branch", default="main", help="Branch name") @@ -83,130 +94,104 @@ def parse(repo, branch): repo = os.path.abspath(repo) click.secho(f"Starting parsing for repository: {repo}", fg="blue", bold=True) - try: - project_id = api_wrapper.parse_project(repo, branch) - - max_attempts: int = 30 - attempt: int = 0 + project_id = api_wrapper.parse_project(repo, branch) - while attempt < max_attempts: - status = api_wrapper.parse_status(project_id) - logging.info(f"Current status: {status}") + max_attempts: int = 30 + for _ in range(max_attempts): + status = api_wrapper.parse_status(project_id) + logging.info("Current status: %s", status) - if status in ["ready", "error"]: - - if status == "ready": - click.secho("Parsing completed", fg="green", bold=True) - else: - click.secho("Parsing failed", fg="red", bold=True) - break - - loading_animation("Parsing in progress") - attempt += 1 - - if attempt >= max_attempts: - logging.warning("Parsing took too long...") - click.secho("Tips to resolve this...", fg="cyan", bold=True) - click.secho( - "This can be happened due to large repository size, so wait for some time.", - fg="yellow", - ) - click.secho( - "Then manually check the parsing status using 'potpie projects'", - fg="yellow", - ) + if status in ["ready", "error"]: + if status == "ready": + click.secho("Parsing completed", fg="green", bold=True) + else: + click.secho("Parsing failed", fg="red", bold=True) + break - except Exception as e: - logging.error(f"Error during parsing: {e}") + loading_animation("Parsing in progress") + else: + logging.warning("Parsing took too long...") + click.secho("Tips to resolve this...", fg="cyan", bold=True) + click.secho( + "This can be happened due to large repository size, so wait for some time.", + fg="yellow", + ) + click.secho( + "Then manually check the parsing status using 'potpie projects'", + fg="yellow", + ) +@handle_error @cli.command() @click.option("--delete", is_flag=True, help="Enable project deletion mode") def projects(delete): """List all projects and optionally delete selected projects""" - try: - projects = api_wrapper.get_list_of_projects() + projects = api_wrapper.get_list_of_projects() - if not delete: - # Standard project listing - table_data = [ - [project["id"], project["repo_name"], project["status"]] - for project in projects - ] - table = tabulate( - table_data, headers=["ID", "Name", "Status"], tablefmt="fancy_grid" + if not delete: + # Standard project listing + table_data = [ + [project["id"], project["repo_name"], project["status"]] + for project in projects + ] + table = tabulate( + table_data, headers=["ID", "Name", "Status"], tablefmt="fancy_grid" + ) + click.echo(table) + else: + # Simple delete mode + click.echo("Available Projects:") + for idx, project in enumerate(projects, 1): + click.echo(f"{idx}. {project['repo_name']} (ID: {project['id']})") + + try: + selection = click.prompt( + "Enter the number of the project to delete", type=int ) - click.echo(table) - else: - # Simple delete mode - click.echo("Available Projects:") - for idx, project in enumerate(projects, 1): - click.echo(f"{idx}. {project['repo_name']} (ID: {project['id']})") - - try: - selection = click.prompt( - "Enter the number of the project to delete", type=int + if 1 <= selection <= len(projects): + selected_project = projects[selection - 1] + selected_project_id = selected_project["id"] + + confirm = click.confirm( + f"Delete {selected_project['repo_name']} with (ID: {selected_project_id})?" ) - if 1 <= selection <= len(projects): - selected_project = projects[selection - 1] - selected_project_id = selected_project["id"] - - confirm = click.confirm( - f"Delete {selected_project['repo_name']} with (ID: {selected_project_id})?" - ) - - if confirm: - status_code = api_wrapper.delete_project(selected_project_id) - - if status_code == 200: - click.echo(f"Project {selected_project['repo_name']}") - click.echo( - f"ID: {selected_project_id}) deleted successfully." - ) - else: - click.echo( - f"Failed to delete project. Status code: {status_code}" - ) - else: - click.echo("Invalid project selection.") - except ValueError: - click.echo("Invalid input. Please enter a valid project number.") - - except requests.RequestException as e: - logging.error(f"Network error occurred: {e}") - except Exception as e: - logging.error(f"An unexpected error occurred: {e}") + + if confirm: + status_code = api_wrapper.delete_project(selected_project_id) + + if status_code == 200: + click.echo(f"Project {selected_project['repo_name']}") + click.echo(f"ID: {selected_project_id}) deleted successfully.") + else: + click.echo( + f"Failed to delete project. Status code: {status_code}" + ) + else: + click.echo("Invalid project selection.") + except ValueError: + click.echo("Invalid input. Please enter a valid project number.") @cli.group() def conversation(): """Talk with your conversations""" - pass - -def handle_api_error(func): - """Decorator for handling API errors""" - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except requests.RequestException as e: - logging.error("Network error occurred: %s", e) - except Exception as e: - logging.error("An unexpected error occurred: %s", e) - return wrapper +@handle_error @conversation.command() @click.argument("title") @click.option("--max-length", default=100, help="Maximum title length") -@handle_api_error def create(title, max_length): """Create a new conversation""" if not title.strip(): click.secho("Title cannot be empty", fg="red") return if len(title) > max_length: - click.secho(f"Title exceeds maximum length of {max_length} characters", fg="red") + click.secho( + f"Title exceeds maximum length of {max_length} characters", fg="red" + ) return # Sees that user_id is used as the defaultUsername @@ -237,10 +222,6 @@ def create(title, max_length): click.echo("Invalid project selection.") except ValueError: click.echo("Invalid input. Please enter a valid project number.") - except requests.RequestException as e: - logging.error("Network error occurred: %s", e) - except Exception as e: - logging.error("An unexpected error occurred: %s", e) try: agents = api_wrapper.available_agents(system_agent=True) @@ -252,11 +233,10 @@ def create(title, max_length): "Enter the number of the agent to start conversation with", type=int ) if 1 <= selection <= len(agents): - selected_agent = agents[selection - 1] - selected_agent_id: str = selected_agent["id"] + selected_agent_id = agents[selection - 1]["id"] confirm = click.confirm( - f"Wanna choose this {selected_agent['name']} agent?" + f"Wanna choose this {agents[selection -1]['name']} agent?" ) if confirm: @@ -264,26 +244,20 @@ def create(title, max_length): else: click.echo("Invalid agent selection.") - except requests.RequestException as e: - logging.error(f"Network error occurred: {e}") - except Exception as e: - logging.error(f"An unexpected error occurred: {e}") + except ValueError: + click.echo("Invalid input. Please enter a valid project number.") - try: - conversation = api_wrapper.create_conversation( - title=title, - project_id_list=[project_ids], - agent_id_list=[agent_id], - ) - click.secho("Conversation created successfully.", fg="green", bold=True) - print(f"Conversation {conversation}") - except requests.RequestException as e: - logging.error(f"Network error occurred: {e}") - except Exception as e: - logging.error(f"An unexpected error occurred: {e}") + conversation = api_wrapper.create_conversation( + title=title, + project_id_list=[project_ids], + agent_id_list=[agent_id], + ) + click.secho("Conversation created successfully.", fg="green", bold=True) + print(f"Conversation {conversation}") + +@handle_error @conversation.command(name="list") -@handle_api_error def list_conversations(): """List all conversations""" conversations = api_wrapper.get_conversation() @@ -296,43 +270,36 @@ def list_conversations(): ) click.echo(table) + @conversation.command() def message(): """Communicate with the agent""" asyncio.run(_message()) +@handle_error async def _message(): """Actual async function for message handling""" conversation_id: str = "" - try: - conversations: dict = api_wrapper.get_conversation() + conversations: dict = api_wrapper.get_conversation() - for idx, conversation in enumerate(conversations, 1): - click.echo( - f"{idx}. {conversation.get('title')} (ID: {conversation.get('id')})" - ) + for idx, conversation in enumerate(conversations, 1): + click.echo(f"{idx}. {conversation.get('title')} (ID: {conversation.get('id')})") - selection = click.prompt( - "Enter the number of the conversation to start messaging with", type=int - ) - if 1 <= selection <= len(conversations): - selected_conversation = conversations[selection - 1] - selected_conversation_id: str = selected_conversation["id"] - - confirm = click.confirm( - f"Wanna start messaging with {selected_conversation['title']}?" - ) - - if confirm: - conversation_id = selected_conversation_id + selection = click.prompt( + "Enter the number of the conversation to start messaging with", type=int + ) + if 1 <= selection <= len(conversations): + selected_conversation = conversations[selection - 1] + selected_conversation_id: str = selected_conversation["id"] - except requests.RequestException as e: - logging.error("Network error occurred: %s", e) + confirm = click.confirm( + f"Wanna start messaging with {selected_conversation['title']}?" + ) - except Exception as e: - logging.error("An unexpected error occurred: %s", e) + if confirm: + conversation_id = selected_conversation_id # Interactive chat loop @@ -356,6 +323,10 @@ async def _message(): print("\nExiting chat session.") break + except Exception as e: + logging.error("An unexpected error occurred: %s", e) + break + if __name__ == "__main__": cli() diff --git a/cli/src/potpie/server_manager.py b/cli/src/potpie/server_manager.py index 8a526f96..5d5cf8bf 100644 --- a/cli/src/potpie/server_manager.py +++ b/cli/src/potpie/server_manager.py @@ -170,41 +170,69 @@ def start_docker(self): "Docker containers does not run within the time 60 seconds" ) - raise DockerError("Docker containers failed to start in time.") from None + raise DockerError( + "Docker containers failed to start in time." + ) from None logging.info("Docker containers started successfully") except subprocess.CalledProcessError as e: logging.error(f"Failed to start Docker containers: {e}") raise DockerError("Failed to start Docker containers") from e - def check_postgres(self) -> bool: - """Check if PostgreSQL server is running""" + + def check_postgres(self, max_retries=5, retry_delay=2) -> bool: + """ + Check if PostgreSQL server is running with retries + + Args: + max_retries (int): Maximum number of retry attempts + retry_delay (int): Delay in seconds between retries + """ logging.info("Checking if PostgreSQL server is running...") - try: - result = subprocess.run( - ["docker", "exec", "potpie_postgres", "pg_isready", "-U", "postgres"], - capture_output=True, - text=True, - ) + + for attempt in range(max_retries): + try: + result = subprocess.run( + ["docker", "exec", "potpie_postgres", "pg_isready", "-U", "postgres"], + capture_output=True, + text=True, + ) - if ( - result.returncode == 0 - and "accepting connections" in result.stdout.lower() - ): - logging.info("PostgreSQL is running and accepting connections") - return True + if result.returncode == 0 and "accepting connections" in result.stdout.lower(): + logging.info("PostgreSQL is running and accepting connections") + return True + + if attempt < max_retries - 1: + logging.warning( + f"PostgreSQL not ready (attempt {attempt + 1}/{max_retries}), " + f"waiting {retry_delay} seconds..." + ) + time.sleep(retry_delay) + continue + + logging.error( + "PostgreSQL check failed: %s", + result.stderr.strip() or result.stdout.strip(), + ) + raise PostgresError( + "PostgreSQL check failed: %s" + % (result.stderr.strip() or result.stdout.strip()) + ) from None + + except subprocess.CalledProcessError as e: + if attempt < max_retries - 1: + logging.warning( + f"Failed to check PostgreSQL status (attempt {attempt + 1}/{max_retries}), " + f"waiting {retry_delay} seconds..." + ) + time.sleep(retry_delay) + continue + + logging.error(f"Failed to check PostgreSQL status: {e}") + raise PostgresError("Failed to check PostgreSQL status") from e + + return False - logging.error( - "PostgreSQL check failed: %s", - result.stderr.strip() or result.stdout.strip(), - ) - raise PostgresError( - "PostgreSQL check failed: %s" - % (result.stderr.strip() or result.stdout.strip()) - ) from None - except subprocess.CalledProcessError as e: - logging.error(f"Failed to check PostgreSQL status: {e}") - raise PostgresError("Failed to check PostgreSQL status") from e def run_migrations(self) -> None: """Run database migrations using alembic from virtual environment""" logging.info("Running database migrations...") @@ -365,7 +393,9 @@ def stop_server(self): logging.info("Stopping Docker containers...") try: - subprocess.run(["docker", "compose", "down"], ) + subprocess.run( + ["docker", "compose", "down"], + ) logging.info("Docker containers stopped") except subprocess.CalledProcessError as e: logging.error("Failed to stop Docker containers: %s", e) diff --git a/cli/test/test_server_manager.py b/cli/test/test_server_manager.py index 9b580f5d..56f7d299 100644 --- a/cli/test/test_server_manager.py +++ b/cli/test/test_server_manager.py @@ -75,6 +75,9 @@ def __init__(self, returncode): self.stdout = "" self.stderr = "" + def poll(self): + return True + if docker_compose_up is DockerError: raise DockerError("Docker Compose failed") return MockProcess(docker_compose_up)