Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions packages/prime/src/prime_cli/commands/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from typing import Optional

import typer
from rich.console import Console

from prime_cli import __version__
from prime_cli.core import APIClient, APIError, Config

app = typer.Typer(
help="Submit feedback about Prime.",
no_args_is_help=False,
invoke_without_command=True,
)
console = Console()


def send_feedback(
message: str,
product: str,
category: str = "general",
run_id: Optional[str] = None,
) -> None:
cfg = Config()

payload = {
"message": message,
"product": product,
"category": category,
"run_id": run_id,
"cli_version": __version__,
"user_id": cfg.user_id,
"team_id": cfg.team_id,
"team_name": cfg.team_name,
}

client = APIClient()
client.post("/feedback", json=payload)


def prompt_for_feedback() -> tuple[Optional[str], str, str, Optional[str]]:
console.print("\n[bold]Prime Feedback[/bold]")
console.print("[dim]Share bugs, feature ideas, or general thoughts.[/dim]\n")

# Product selection
console.print("[bold]Product:[/bold]")
console.print(" 1. Hosted RL")
console.print(" 2. Other")

try:
product_choice = typer.prompt("\nSelect", type=int, default=1)
except (KeyboardInterrupt, typer.Abort):
return None, "other", "general", None

product = {1: "hosted rl"}.get(product_choice, "other")

# Category selection
console.print("\n[bold]Feedback type:[/bold]")
console.print(" 1. Bug report")
console.print(" 2. Feature request")
console.print(" 3. General feedback")

try:
category_choice = typer.prompt("\nSelect", type=int, default=3)
except (KeyboardInterrupt, typer.Abort):
return None, product, "general", None

category = {1: "bug", 2: "feature"}.get(category_choice, "general")

# Run ID prompt (only for hosted rl)
run_id = None
if product == "hosted rl":
console.print(
"\n[dim]If related to a specific run, enter Run ID (or Enter to skip)[/dim]"
)
try:
run_id = typer.prompt("Run ID", default="", show_default=False).strip() or None
except (KeyboardInterrupt, typer.Abort):
return None, product, category, None

# Feedback message
console.print("\n[bold]Enter your feedback:[/bold]")

try:
message = typer.prompt("").strip()
except (KeyboardInterrupt, typer.Abort):
return None, product, category, None

return message if message else None, product, category, run_id


@app.callback(invoke_without_command=True)
def feedback(ctx: typer.Context) -> None:
"""Submit feedback about Prime.

Example:
prime feedback
"""
if ctx.invoked_subcommand is not None:
return

message, product, category, run_id = prompt_for_feedback()
if not message:
console.print("\n[yellow]Cancelled[/yellow]")
raise typer.Exit(0)

try:
with console.status("Submitting...", spinner="dots"):
send_feedback(message, product, category, run_id)
console.print("[green]Feedback submitted. Thanks![/green]")
except APIError as e:
console.print(f"[red]Error:[/red] {str(e)}")
raise typer.Exit(1)
2 changes: 2 additions & 0 deletions packages/prime/src/prime_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .commands.disks import app as disks_app
from .commands.env import app as env_app
from .commands.evals import app as evals_app
from .commands.feedback import app as feedback_app
from .commands.images import app as images_app
from .commands.inference import app as inference_app
from .commands.lab import app as lab_app
Expand Down Expand Up @@ -51,6 +52,7 @@
app.add_typer(whoami_app, name="whoami", rich_help_panel="Account")
app.add_typer(config_app, name="config", rich_help_panel="Account")
app.add_typer(teams_app, name="teams", rich_help_panel="Account")
app.add_typer(feedback_app, name="feedback", rich_help_panel="Account")
app.add_typer(upgrade_app, name="upgrade", rich_help_panel="Account")


Expand Down
191 changes: 191 additions & 0 deletions packages/prime/src/prime_cli/utils/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""Feedback submission utility."""

import os
from datetime import datetime, timezone
from typing import Optional

import httpx
import typer
from rich.console import Console

from prime_cli import __version__
from prime_cli.core import Config

console = Console()

SLACK_WEBHOOK_URL = os.getenv(
"PRIME_FEEDBACK_WEBHOOK",
"https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
)


def _get_user_info() -> dict:
"""Fetch user info from API."""
from prime_cli.core import APIClient, APIError

cfg = Config()
info = {
"user_id": cfg.user_id,
"username": None,
"email": None,
"team_id": cfg.team_id,
"team_name": cfg.team_name,
}

if cfg.api_key:
try:
client = APIClient()
response = client.get("/user/whoami")
data = response.get("data", {})
if isinstance(data, dict):
info["username"] = data.get("slug") or data.get("name")
info["email"] = data.get("email")
except (APIError, Exception):
pass

return info


def send_feedback(
message: str,
product: str,
category: str = "general",
run_id: Optional[str] = None,
) -> bool:
"""Send feedback to Slack.

Args:
message: The feedback message
product: One of 'hosted rl' or 'other'
category: One of 'bug', 'feature', or 'general'
run_id: Optional run ID related to the feedback

Returns:
True if sent successfully, False otherwise
"""
user_info = _get_user_info()
submitted_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")

if user_info["username"] and user_info["user_id"]:
user_display = f"{user_info['username']} ({user_info['user_id']})"
elif user_info["user_id"]:
user_display = user_info["user_id"]
else:
user_display = "anonymous"

if user_info["team_name"] and user_info["team_id"]:
team_display = f"{user_info['team_name']} ({user_info['team_id']})"
elif user_info["team_id"]:
team_display = user_info["team_id"]
else:
team_display = "personal"

fields = [
f"*Datetime submitted:* {submitted_at}",
f"*CLI version:* {__version__}",
f"*User:* {user_display}",
f"*Email:* {user_info['email'] or 'N/A'}",
f"*Team:* {team_display}",
f"*Product:* {product}",
f"*Feedback type:* {category}",
]

if run_id:
fields.append(f"*Run ID:* {run_id}")

fields.append(f"*Feedback:* {message}")

payload = {
"blocks": [
{
"type": "header",
"text": {"type": "plain_text", "text": "Feedback Submission", "emoji": False},
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": "\n".join(fields)},
},
],
"text": f"Feedback ({product} / {category}): {message[:80]}",
}

try:
resp = httpx.post(SLACK_WEBHOOK_URL, json=payload, timeout=10.0)
return resp.status_code == 200
except httpx.RequestError:
return False


def prompt_for_feedback() -> tuple[Optional[str], str, str, Optional[str]]:
"""Interactive prompt for feedback.

Returns:
Tuple of (message, product, category, run_id) or (None, ...) if cancelled
"""
console.print("\n[bold]Prime Feedback[/bold]")
console.print("[dim]Share bugs, feature ideas, or general thoughts.[/dim]\n")

# Product selection
console.print("[bold]Product:[/bold]")
console.print(" 1. Hosted RL")
console.print(" 2. Other")

try:
product_choice = typer.prompt("\nSelect", type=int, default=1)
except (KeyboardInterrupt, typer.Abort):
return None, "other", "general", None

product = {1: "hosted rl"}.get(product_choice, "other")

# Category selection
console.print("\n[bold]Feedback type:[/bold]")
console.print(" 1. Bug report")
console.print(" 2. Feature request")
console.print(" 3. General feedback")

try:
category_choice = typer.prompt("\nSelect", type=int, default=3)
except (KeyboardInterrupt, typer.Abort):
return None, product, "general", None

category = {1: "bug", 2: "feature"}.get(category_choice, "general")

# Run ID prompt (only for hosted rl)
run_id = None
if product == "hosted rl":
console.print(
"\n[dim]If related to a specific run, enter Run ID (or Enter to skip)[/dim]"
)
try:
run_id = typer.prompt("Run ID", default="", show_default=False).strip() or None
except (KeyboardInterrupt, typer.Abort):
return None, product, category, None

# Feedback message
console.print("\n[bold]Enter your feedback:[/bold]")

try:
message = typer.prompt("").strip()
except (KeyboardInterrupt, typer.Abort):
return None, product, category, None

return message if message else None, product, category, run_id


def run_feedback_command() -> None:
"""Main entry point for the feedback command."""
message, product, category, run_id = prompt_for_feedback()
if not message:
console.print("\n[yellow]Cancelled[/yellow]")
raise typer.Exit(0)

with console.status("Submitting...", spinner="dots"):
success = send_feedback(message, product, category, run_id)

if success:
console.print("[green]Feedback submitted. Thanks![/green]")
else:
console.print(
"[red]Failed to send. Please try again or email support@primeintellect.ai[/red]"
)
raise typer.Exit(1)