Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): Add rich text for cli #1108

Merged
merged 16 commits into from
Jan 30, 2025
Merged

Conversation

HamadaSalhab
Copy link
Contributor

@HamadaSalhab HamadaSalhab commented Jan 29, 2025

PR Type

Enhancement, Tests


Description

  • Introduced rich text and progress indicators for CLI commands.

  • Enhanced error handling and user feedback in CLI operations.

  • Refactored CLI commands for agents, tasks, tools, and executions.

  • Improved table and panel formatting for better output visualization.


Changes walkthrough 📝

Relevant files
Enhancement
11 files
agents.py
Added rich text and progress indicators for agent commands.
+168/-44
chat.py
Enhanced chat command with rich text and error handling. 
+49/-14 
executions.py
Improved execution creation with progress and error handling.
+29/-6   
importt.py
Enhanced agent import with progress and detailed feedback.
+146/-48
init.py
Added markdown rendering and improved initialization feedback.
+26/-10 
logs.py
Improved logs command with table formatting and progress indicators.
+55/-14 
ls.py
Enhanced listing of entities with formatted tables.           
+58/-26 
sync.py
Improved synchronization with progress indicators and detailed
feedback.
+296/-116
tasks.py
Enhanced task management with rich text and error handling.
+142/-43
tools.py
Improved tool management with progress indicators and detailed output.
+132/-27
utils.py
Refactored utility functions for better integration with CLI
enhancements.
+2/-2     

Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.

  • Important

    This pull request enhances the CLI with rich text, progress indicators, and improved error handling, refactoring commands for better output visualization.

    • CLI Enhancements:
      • Added rich text and progress indicators to CLI commands in agents.py, chat.py, and executions.py.
      • Improved error handling and user feedback in CLI operations.
      • Enhanced table and panel formatting for better output visualization.
    • Refactoring:
      • Refactored CLI commands for agents, tasks, tools, and executions.
      • Updated utility functions in utils.py for better integration with CLI enhancements.
    • Miscellaneous:
      • Added markdown rendering in init.py.
      • Improved synchronization feedback in sync.py.
      • Enhanced listing of entities in ls.py.

    This description was created by Ellipsis for 193a4a0. It will automatically update as commits are pushed.

    @HamadaSalhab HamadaSalhab marked this pull request as ready for review January 30, 2025 01:29
    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Error Handling

    The sync command has complex error handling paths that should be validated, particularly around file operations and API calls. The error messages and recovery paths should be reviewed for correctness.

                "--source",
                "-s",
                help="Source directory containing julep.yaml",
            ),
        ] = Path.cwd(),
        force_local: Annotated[
            bool,
            typer.Option(
                "--force-local",
                help="Force local state to match remote",
            ),
        ] = False,
        force_remote: Annotated[
            bool,
            typer.Option(
                "--force-remote",
                help="Force remote state to match local",
            ),
        ] = False,
        dry_run: Annotated[
            bool,
            typer.Option(
                "--dry-run",
                "-d",
                help="Simulate synchronization without making changes",
            ),
        ] = False,
    ):
        """Synchronize local package with Julep platform"""
    
        if dry_run:
            console.print(
                Text("Dry run - no changes will be made", style="bold yellow"))
            return
    
        if force_local and force_remote:
            error_console.print(Text(
                "Error: Cannot use both --force-local and --force-remote", style="bold red"))
            raise typer.Exit(1)
    
        lock_file = source / "julep-lock.json"
    
        try:
            client = get_julep_client()
        except Exception as e:
            error_console.print(
                Text(f"Error initializing Julep client: {e}", style="bold red"))
            raise typer.Exit(1)
    
        if force_local and not lock_file.exists():
            locked_agents: list[LockedEntity] = []
            locked_tasks: list[LockedEntity] = []
            locked_tools: list[LockedEntity] = []
            relationships: Relationships = Relationships()
    
            julep_yaml_content = get_julep_yaml(source)
            agents: list[dict] = julep_yaml_content.get("agents", [])
            tasks: list[dict] = julep_yaml_content.get("tasks", [])
            tools: list[dict] = julep_yaml_content.get("tools", [])
    
            if agents or tasks or tools:
                console.print(Panel(
                    Text("Found the following new entities in julep.yaml:", style="bold cyan")))
                from rich.table import Table
    
                if agents:
                    table = Table(title="Agents", show_header=False, title_style="bold magenta")
                    table.add_column("Definition", style="dim", width=50)
                    for agent in agents:
                        table.add_row(agent.get('definition'))
                    console.print(table)
    
                if tasks:
                    table = Table(title="Tasks", show_header=False, title_style="bold cyan")
                    table.add_column("Definition", style="dim", width=50)
                    for task in tasks:
                        table.add_row(task.get('definition'))
                    console.print(table)
    
                if tools:
                    table = Table(title="Tools", show_header=False, title_style="bold green")
                    table.add_column("Definition", style="dim", width=50)
                    for tool in tools:
                        table.add_row(tool.get('definition'))
                    console.print(table)
    
            else:
                console.print(
                    Text("No new entities found in julep.yaml", style="bold yellow"))
                return
    
            if agents:
                for agent in agents:
                    agent_yaml_path: Path = source / agent.pop("definition")
                    agent_yaml_content = yaml.safe_load(
                        agent_yaml_path.read_text())
    
                    agent_request_hash = hashlib.sha256(
                        json.dumps(agent_yaml_content).encode()
                    ).hexdigest()
    
                    agent_request = CreateAgentRequest(
                        **agent_yaml_content, **agent)
    
                    with Progress(
                        SpinnerColumn(),
                        TextColumn("[progress.description]{task.description}"),
                        transient=True,
                        console=console
                    ) as progress:
                        sync_task = progress.add_task("Creating agents on remote...", start=False)
                        progress.start_task(sync_task)
    
                        created_agent = client.agents.create(
                            **agent_request.model_dump(exclude_unset=True, exclude_none=True)
                        )
    
                    locked_agents.append(
                        LockedEntity(
                            path=str(agent_yaml_path.relative_to(source)),
                            id=created_agent.id,
                            last_synced=created_agent.created_at.isoformat(
                                timespec="milliseconds")
                            + "Z",
                            revision_hash=agent_request_hash,
                        )
                    )
                console.print(
                    Text("All agents were successfully created on remote", style="bold green"))
    
            if tasks:
                for task in tasks:
                    task_yaml_path: Path = source / task.pop("definition")
    
                    agent_id_expression = task.pop("agent_id")
                    agent_id = eval(f'f"{agent_id_expression}"', {
                                    "agents": locked_agents})
    
                    task_yaml_content = yaml.safe_load(task_yaml_path.read_text())
    
                    task_request_hash = hashlib.sha256(
                        json.dumps(task_yaml_content).encode()
                    ).hexdigest()
    
                    task_request = CreateTaskRequest(**task_yaml_content, **task)
    
                    with Progress(
                        SpinnerColumn(),
                        TextColumn("[progress.description]{task.description}"),
                        transient=True,
                        console=console
                    ) as progress:
                        sync_task = progress.add_task("Creating tasks on remote...", start=False)
                        progress.start_task(sync_task)
    
                        created_task = client.tasks.create(
                            agent_id=agent_id,
                            **task_request.model_dump(exclude_unset=True, exclude_none=True),
                        )
    
                    relationships.tasks.append(
                        TaskAgentRelationship(
                            id=created_task.id, agent_id=agent_id)
                    )
    
                    locked_tasks.append(
                        LockedEntity(
                            path=str(task_yaml_path.relative_to(source)),
                            id=created_task.id,
                            last_synced=created_task.created_at.isoformat(
                                timespec="milliseconds")
                            + "Z",
                            revision_hash=task_request_hash,
                        )
                    )
                console.print(
                    Text("All tasks were successfully created on remote", style="bold blue"))
    
            if tools:
                for tool in tools:
                    tool_yaml_path: Path = source / tool.pop("definition")
                    tool_yaml_content = yaml.safe_load(tool_yaml_path.read_text())
    
                    tool_request_hash = hashlib.sha256(
                        json.dumps(tool_yaml_content).encode()
                    ).hexdigest()
    
                    tool_request = CreateToolRequest(**tool_yaml_content, **tool)
    
                    agent_id_expression = tool.pop("agent_id")
                    agent_id = eval(f'f"{agent_id_expression}"', {
                                    "agents": locked_agents})
    
                    with Progress(
                        SpinnerColumn(),
                        TextColumn("[progress.description]{task.description}"),
                        transient=True,
                        console=console
                    ) as progress:
                        sync_task = progress.add_task("Creating tools on remote...", start=False)
                        progress.start_task(sync_task)
    
                        created_tool = client.agents.tools.create(
                            agent_id=agent_id,
                            **tool_request.model_dump(exclude_unset=True, exclude_none=True),
                        )
    
                    relationships.tools.append(
                        ToolAgentRelationship(
                            id=created_tool.id, agent_id=agent_id)
                    )
    
                    locked_tools.append(
                        LockedEntity(
                            path=str(tool_yaml_path.relative_to(source)),
                            id=created_tool.id,
                            last_synced=created_tool.created_at.isoformat(
                                timespec="milliseconds")
                            + "Z",
                            revision_hash=tool_request_hash,
                        )
                    )
                console.print(
                    Text("All tools were successfully created on remote", style="bold magenta"))
    
    
            with Progress(
                SpinnerColumn(),
                TextColumn("[progress.description]{task.description}"),
                transient=True,
                console=console
            ) as progress:
                sync_task = progress.add_task("Writing lock file...", start=False)
                progress.start_task(sync_task)
    
                write_lock_file(
                    source,
                    LockFileContents(
                    agents=locked_agents,
                    tasks=locked_tasks,
                    tools=locked_tools,
                    relationships=relationships,
                ),
            )
    
            console.print(Text("Synchronization complete", style="bold green"))
    
            return
    
        if force_local and lock_file.exists():
            found_changes = False
    
            lock_file = get_lock_file(source)
            julep_yaml_content = get_julep_yaml(source)
    
            agents_julep_yaml = julep_yaml_content.get("agents", [])
            tasks_julep_yaml = julep_yaml_content.get("tasks", [])
            tools_julep_yaml = julep_yaml_content.get("tools", [])
    
            for agent_julep_yaml in agents_julep_yaml:
                agent_yaml_def_path: Path = Path(
                    agent_julep_yaml.get("definition"))
                agent_yaml_content = yaml.safe_load(
                    (source / agent_yaml_def_path).read_text())
                found_in_lock = False
    
                for i in range(len(lock_file.agents)):
                    agent_julep_lock = lock_file.agents[i]
    
                    if agent_julep_lock.path == str(agent_yaml_def_path):
                        found_in_lock = True
                        agent_yaml_content_hash = hashlib.sha256(
                            json.dumps(agent_yaml_content).encode()).hexdigest()
                        agent_julep_lock_hash = agent_julep_lock.revision_hash
    
                        if agent_yaml_content_hash != agent_julep_lock_hash:
                            found_changes = True
                            console.print(Text(f"Agent {agent_yaml_def_path} has changed, updating on remote...", style="bold yellow"))
    
                            agent_request = CreateAgentRequest(
                                **agent_yaml_content, **agent_julep_yaml)
    
                            with Progress(
                                SpinnerColumn(),
                                TextColumn("[progress.description]{task.description}"),
                                transient=True,
                                console=console
                            ) as progress:
                                sync_task = progress.add_task("Updating agent on remote...", start=False)
                                progress.start_task(sync_task)
    
                                try:
                                    client.agents.create_or_update(
                                        agent_id=agent_julep_lock.id, **agent_request.model_dump(exclude_unset=True, exclude_none=True))
                                except Exception as e:
                                    error_console.print(
                                        f"[bold red]Error updating agent: {e}[/bold red]")
                                    raise typer.Exit(1)
    
                            console.print(
                                Text(f"Agent {agent_yaml_def_path} updated successfully", style="bold blue"))
    
                            updated_at = client.agents.get(
                                agent_julep_lock.id).updated_at
                            lock_file.agents[i] = LockedEntity(
                                path=agent_julep_lock.path,
                                id=agent_julep_lock.id,
                                last_synced=updated_at.isoformat(
                                    timespec="milliseconds") + "Z",
                                revision_hash=agent_yaml_content_hash,
                            )
                        break
    
                if not found_in_lock:
                    console.print(Text(f"Agent {agent_yaml_def_path} not found in lock file.", style="bold yellow"))
    
                    agent_request_hash = hashlib.sha256(
                        json.dumps(agent_yaml_content).encode()).hexdigest()
    
                    agent_request = CreateAgentRequest(
                        **agent_yaml_content, **agent_julep_yaml)
    
                    with Progress(
                        SpinnerColumn(),
                        TextColumn("[progress.description]{task.description}"),
                        transient=True,
                        console=console
                    ) as progress:
                        sync_task = progress.add_task("Creating agent on remote...", start=False)
                        progress.start_task(sync_task)
    
                        created_agent = client.agents.create(
                            **agent_request.model_dump(exclude_unset=True, exclude_none=True))
    
                    console.print(Text(f"Agent {agent_yaml_def_path} created successfully on remote", style="bold blue"))
    
                    lock_file.agents.append(LockedEntity(
                        path=str(agent_yaml_def_path),
                        id=created_agent.id,
                        last_synced=created_agent.created_at.isoformat(
                            timespec="milliseconds") + "Z",
                        revision_hash=agent_request_hash,
                    ))
    
            for task_julep_yaml in tasks_julep_yaml:
                task_yaml_def_path: Path = Path(task_julep_yaml.get("definition"))
                task_yaml_content = yaml.safe_load(
                    (source / task_yaml_def_path).read_text())
                found_in_lock = False
    
                for i in range(len(lock_file.tasks)):
                    task_julep_lock = lock_file.tasks[i]
    
                    if task_julep_lock.path == str(task_yaml_def_path):
                        found_in_lock = True
                        task_yaml_content_hash = hashlib.sha256(
                            json.dumps(task_yaml_content).encode()).hexdigest()
                        task_julep_lock_hash = task_julep_lock.revision_hash
    
                        if task_yaml_content_hash != task_julep_lock_hash:
                            console.print(Text(f"Task {task_yaml_def_path} has changed, updating on remote...", style="bold yellow"))
                            found_changes = True
    
                            task_request = CreateTaskRequest(
                                **task_yaml_content, **task_julep_yaml)
    
                            agent_id = get_related_agent_id(
                                task_julep_lock.id, lock_file.relationships.tasks)
    
                            if not agent_id:
                                error_console.print(Text(f"Task {task_yaml_def_path} has no related agent. Please check the lock file and julep.yaml for consistency.", style="bold red"))
                                raise typer.Exit(1)
    
                            with Progress(
                                SpinnerColumn(),
                                TextColumn("[progress.description]{task.description}"),
                                transient=True,
                                console=console
                            ) as progress:
                                sync_task = progress.add_task("Updating task on remote...", start=False)
                                progress.start_task(sync_task)
    
                                client.tasks.create_or_update(
                                    task_id=task_julep_lock.id, agent_id=agent_id, **task_request.model_dump(exclude_unset=True, exclude_none=True))
    
                            console.print(Text(f"Task {task_yaml_def_path} updated successfully", style="bold blue"))
    
                            updated_at = client.tasks.get(
                                task_julep_lock.id).updated_at
                            lock_file.tasks[i] = LockedEntity(
                                path=task_julep_lock.path,
                                id=task_julep_lock.id,
                                last_synced=updated_at.isoformat(
                                    timespec="milliseconds") + "Z",
                                revision_hash=task_yaml_content_hash,
                            )
                        break
    
                if not found_in_lock:
                    console.print(Text(f"Task {task_yaml_def_path} not found in lock file, creating new task...", style="bold yellow"))
    
                    task_request_hash = hashlib.sha256(
                        json.dumps(task_yaml_content).encode()).hexdigest()
    
                    agent_id_expression = task_julep_yaml.pop("agent_id")
                    agent_id = eval(f'f"{agent_id_expression}"', {
                                    "agents": lock_file.agents})
    
                    task_request = CreateTaskRequest(
                        **task_yaml_content, **task_julep_yaml)
    
                    with Progress(
                        SpinnerColumn(),
                        TextColumn("[progress.description]{task.description}"),
                        transient=True,
                        console=console
                    ) as progress:
                        sync_task = progress.add_task("Creating task on remote...", start=False)
                        progress.start_task(sync_task)
    
                        created_task = client.tasks.create(
                            agent_id=agent_id, **task_request.model_dump(exclude_unset=True, exclude_none=True))
    
                    lock_file.tasks.append(LockedEntity(
                        path=str(task_yaml_def_path),
                        id=created_task.id,
                        last_synced=created_task.created_at.isoformat(
                            timespec="milliseconds") + "Z",
                        revision_hash=task_request_hash,
                    ))
    
                    lock_file.relationships.tasks.append(
                        TaskAgentRelationship(id=created_task.id, agent_id=agent_id))
    
                    console.print(Text(
                        f"Task {task_yaml_def_path} created successfully on remote", style="bold blue"))
    
            for tool_julep_yaml in tools_julep_yaml:
                tool_yaml_def_path: Path = Path(tool_julep_yaml.get("definition"))
                tool_yaml_content = yaml.safe_load(
                    (source / tool_yaml_def_path).read_text())
                found_in_lock = False
    
                for i in range(len(lock_file.tools)):
                    tool_julep_lock = lock_file.tools[i]
    
                    if tool_julep_lock.path == str(tool_yaml_def_path):
                        found_in_lock = True
    
                        tool_yaml_content_hash = hashlib.sha256(
                            json.dumps(tool_yaml_content).encode()).hexdigest()
                        tool_julep_lock_hash = tool_julep_lock.revision_hash
    
                        if tool_yaml_content_hash != tool_julep_lock_hash:
                            found_changes = True
                            console.print(Text(f"Tool {tool_yaml_def_path} has changed, updating on remote...", style="bold yellow"))
    
                            tool_request = CreateToolRequest(
                                **tool_yaml_content, **tool_julep_yaml)
    
                            agent_id = get_related_agent_id(
                                tool_julep_lock.id, lock_file.relationships.tools)
    
                            if not agent_id:
                                error_console.print(Text(f"Tool {tool_yaml_def_path} has no related agent. Please check the lock file and julep.yaml for consistency.", style="bold red"))
                                raise typer.Exit(1)
    
                            with Progress(
                                SpinnerColumn(),
                                TextColumn("[progress.description]{task.description}"),
                                transient=True,
                                console=console
                            ) as progress:
                                sync_task = progress.add_task("Updating tool on remote...", start=False)
                                progress.start_task(sync_task)
    
                                client.agents.tools.update(tool_id=tool_julep_lock.id, agent_id=agent_id,
                                                           **tool_request.model_dump(exclude_unset=True, exclude_none=True))
    
                            console.print(Text(f"Tool {tool_yaml_def_path} updated successfully", style="bold blue"))
    
                            lock_file.tools[i] = LockedEntity(
                                path=tool_julep_lock.path,
                                id=tool_julep_lock.id,
                                last_synced=datetime.now().isoformat(timespec="milliseconds") + "Z",
                                revision_hash=tool_yaml_content_hash,
                            )
                        break
    
                if not found_in_lock:
                    console.print(Text(f"Tool {tool_yaml_def_path} not found in lock file, creating new tool...", style="bold yellow"))
    
                    tool_request_hash = hashlib.sha256(
                        json.dumps(tool_yaml_content).encode()).hexdigest()
    
                    agent_id_expression = tool_julep_yaml.pop("agent_id")
                    agent_id = eval(f'f"{agent_id_expression}"', {
                                    "agents": lock_file.agents})
    
                    tool_request = CreateToolRequest(
                        **tool_yaml_content, **tool_julep_yaml)
    
                    with Progress(
                        SpinnerColumn(),
                        TextColumn("[progress.description]{task.description}"),
                        transient=True,
                        console=console
                    ) as progress:
                        sync_task = progress.add_task("Creating tool on remote...", start=False)
                        progress.start_task(sync_task)
    
                        created_tool = client.agents.tools.create(
                            agent_id=agent_id, **tool_request.model_dump(exclude_unset=True, exclude_none=True))
    
                    lock_file.tools.append(LockedEntity(
                        path=str(tool_yaml_def_path),
                        id=created_tool.id,
                        last_synced=created_tool.created_at.isoformat(
                            timespec="milliseconds") + "Z",
                        revision_hash=tool_request_hash,
                    ))
    
                    lock_file.relationships.tools.append(
                        ToolAgentRelationship(id=created_tool.id, agent_id=agent_id))
    
                    console.print(Text(
                        f"Tool {tool_yaml_def_path} created successfully on remote", style="bold blue"))
    
            if found_changes:
                with Progress(  
                    SpinnerColumn(),
                    TextColumn("[progress.description]{task.description}"),
                    transient=True,
                    console=console
                ) as progress:
                    sync_task = progress.add_task("Writing lock file...", start=False)
                    progress.start_task(sync_task)
    
                    write_lock_file(source, lock_file)
    
                console.print(Text("Synchronization complete", style="bold green"))
    
                return
    
            else:
                console.print(Text("No changes detected. Everything is up to date.", style="bold green"))
                return
    Input Validation

    The agent creation and update operations need validation of user inputs, especially for JSON metadata and settings. The current validation may allow invalid data through.

        ] = [],
        definition: Annotated[
            str | None, typer.Option("--definition", "-d", help="Path to an agent definition file")
        ] = None,
    ):
        """Create a new AI agent. Either provide a definition file or use the other options."""
    
        if definition:
            # TODO: implement definition file parsing
            error_console.print("Passing definition file is not implemented yet", err=True)
            raise typer.Exit(1)
    
        # Validate that either definition is provided or name/model
        if not definition and not (name and model):
            error_console.print("Error: Must provide either a definition file or name and model", err=True)
            raise typer.Exit(1)
    
        try:
            if metadata:
                json.loads(metadata)
            if default_settings:
                json.loads(default_settings)
        except json.JSONDecodeError as e:
            error_console.print(f"Error parsing JSON: {e}", err=True)
            raise typer.Exit(1)
    
        client = get_julep_client()
    
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            console=console
        ) as progress:
            try:
                create_agent_task = progress.add_task("Creating agent...", start=False)
                progress.start_task(create_agent_task)
    
                agent = client.agents.create(
                    name=name,
                    model=model,
                    about=about,
                    default_settings=default_settings,
                    metadata=metadata,
                    instructions=instructions,
                )
    
            except Exception as e:
                error_console.print(f"Error creating agent: {e}", style="bold red")
                raise typer.Exit(1)
    
        console.print(
            Text(
                f"Agent created successfully. Agent ID: {agent.id}", style="bold green"
            )
        )
    Missing Validation

    The tool operations lack proper validation for required fields and YAML content. This could lead to runtime errors if invalid tool definitions are provided.

    ):
        """Create a new tool for an agent.
    
        Requires either a definition file or direct parameters. If both are provided,
        command-line options override values from the definition file.
        """
    
        client = get_julep_client()
    
        tool_yaml_contents = yaml.safe_load(Path(definition).read_text())
    
        if name:
            tool_yaml_contents["name"] = name
    
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            transient=True,
        ) as progress:
            try:
                create_tool_task = progress.add_task(description="Creating tool", total=None)
                progress.start_task(create_tool_task)
                tool = client.agents.tools.create(agent_id=agent_id, **tool_yaml_contents)
            except Exception as e:
                error_console.print(f"Error creating tool: {e}")
                raise typer.Exit(1)
    
        console.print(Text(f"Tool created successfully. Tool ID: {tool.id}", style="bold green"))
    
    
    @tools_app.command()
    def update(
        agent_id: Annotated[
            str, typer.Option("--agent-id", "-a", help="ID of the agent the tool is associated with")
        ],
        tool_id: Annotated[str, typer.Option("--id", help="ID of the tool to update")],
        definition: Annotated[
            str | None,
            typer.Option(
                "--definition", "-d", help="Path to the updated tool configuration YAML file"
            ),
        ] = None,
        name: Annotated[
            str | None, typer.Option("--name", "-n", help="New name for the tool")
        ] = None,
    ):
        """Update an existing tool's details.
    
        Updates can be made using either a definition file or direct parameters.
        If both are provided, command-line options override values from the definition file.
        """
    
        client = get_julep_client()
    
    
        if definition:
            tool_yaml_contents = yaml.safe_load(Path(definition).read_text())
        else:
            tool_yaml_contents = {}
    
        if name:
            tool_yaml_contents["name"] = name
    
        if not tool_yaml_contents:
            error_console.print("Error: No tool name or definition provided", style="bold red")
            raise typer.Exit(1)
    
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            transient=True,
            console=console
        ) as progress:
            try:
                update_tool_task = progress.add_task("Updating tool...", start=False)
                progress.start_task(update_tool_task)
    
                client.agents.tools.update(agent_id=agent_id, tool_id=tool_id, **tool_yaml_contents)
            except Exception as e:
                error_console.print(f"Error updating tool: {e}", style="bold red")
                raise typer.Exit(1)
    
        console.print(Text(f"Tool updated successfully.", style="bold green"))

    Copy link
    Contributor

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Score
    Security
    Add input validation for eval()

    The eval() function is used to dynamically evaluate agent_id expressions, which can
    be dangerous if the input is not properly sanitized. Consider using a safer method
    for string interpolation or add input validation.

    cli/julep_cli/sync.py [173-174]

    +# Add validation before eval
    +if not isinstance(agent_id_expression, str) or '{' not in agent_id_expression:
    +    raise ValueError("Invalid agent_id expression format")
     agent_id = eval(f'f"{agent_id_expression}"', {"agents": locked_agents})
    • Apply this suggestion
    Suggestion importance[1-10]: 9

    Why: Using eval() without proper validation is a serious security risk. The suggestion adds important input validation to prevent potential code injection attacks.

    9
    Possible issue
    Add missing error handling

    Add error handling for the case when the definition file cannot be read or contains
    invalid YAML. Currently, if the file doesn't exist or has invalid YAML syntax, it
    will raise an unhandled exception.

    cli/julep_cli/tasks.py [68]

    -task_yaml_contents = yaml.safe_load(Path(definition).read_text())
    +try:
    +    task_yaml_contents = yaml.safe_load(Path(definition).read_text())
    +except (yaml.YAMLError, FileNotFoundError) as e:
    +    error_console.print(f"Error reading task definition: {e}", style="bold red")
    +    raise typer.Exit(1)
    • Apply this suggestion
    Suggestion importance[1-10]: 8

    Why: The suggestion addresses a critical error handling gap that could cause the application to crash when encountering invalid YAML files or missing definition files. This is an important defensive programming practice.

    8
    Fix potential IndexError

    The tailing loop should check if transitions list is empty before accessing
    transitions[0] to avoid IndexError when no transitions are available.

    cli/julep_cli/logs.py [83-84]

    -if transitions and transitions[0].type in ["finish", "cancelled", "error"]:
    -            break
    +if not transitions:
    +    time.sleep(1)
    +    continue
    +if transitions[0].type in ["finish", "cancelled", "error"]:
    +    break
    • Apply this suggestion
    Suggestion importance[1-10]: 7

    Why: The suggestion prevents a potential runtime crash by adding a necessary check for empty transitions list before accessing its first element. This is a valid defensive programming fix.

    7
    General
    Standardize error handling patterns

    The error handling for client operations is inconsistent. Some errors are caught and
    displayed with error_console.print(), while others might propagate uncaught.
    Implement consistent error handling across all client operations.

    cli/julep_cli/sync.py [330-335]

     try:
         client.agents.create_or_update(agent_id=agent_julep_lock.id, **agent_request.model_dump(exclude_unset=True, exclude_none=True))
     except Exception as e:
    -    error_console.print(f"[bold red]Error updating agent: {e}[/bold red]")
    +    error_console.print(Text(f"Error updating agent: {e}", style="bold red"))
         raise typer.Exit(1)
    • Apply this suggestion
    Suggestion importance[1-10]: 6

    Why: The suggestion improves code consistency by standardizing error handling using Text objects with consistent styling across the codebase. This enhances maintainability and user experience.

    6

    Copy link
    Contributor

    @ellipsis-dev ellipsis-dev bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    👍 Looks good to me! Reviewed everything up to 37c8f53 in 1 minute and 21 seconds

    More details
    • Looked at 2156 lines of code in 11 files
    • Skipped 0 files when reviewing.
    • Skipped posting 1 drafted comments based on config settings.
    1. cli/julep_cli/utils.py:201
    • Draft comment:
      Update the comment to reflect that this function specifically adds an agent to the julep.yaml file.
    def add_agent_to_julep_yaml(source: Path, agent_data: dict):
        """Add a new agent to the julep.yaml file"""
    
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The function name add_agent_to_julep_yaml was changed from import_agent_to_julep_yaml, but the comment above it still says 'Add a new entity'. It should be updated to reflect that it specifically adds an agent.

    Workflow ID: wflow_9BUQI3tW69c6qq5T


    You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

    Copy link
    Contributor

    @ellipsis-dev ellipsis-dev bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    👍 Looks good to me! Incremental review on 193a4a0 in 31 seconds

    More details
    • Looked at 551 lines of code in 9 files
    • Skipped 0 files when reviewing.
    • Skipped posting 4 drafted comments based on config settings.
    1. cli/julep_cli/importt.py:102
    • Draft comment:
      Unnecessary use of f-string. Simplify to a regular string.
    update_task = progress.add_task("Updating agent in '{agent_yaml_path}'...", start=False)
    
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The code has multiple instances where f-strings are used unnecessarily with static strings. This can be simplified for better readability and performance.
    2. cli/julep_cli/importt.py:172
    • Draft comment:
      Unnecessary use of f-string. Simplify to a regular string.
    update_yaml_for_existing_entity(agent_yaml_path, remote_agent.model_dump(exclude={"id", "created_at", "updated_at"}))
    
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The code has multiple instances where f-strings are used unnecessarily with static strings. This can be simplified for better readability and performance.
    3. cli/julep_cli/importt.py:175
    • Draft comment:
      Unnecessary use of f-string. Simplify to a regular string.
    add_agent_to_julep_yaml(source, {"definition": str(agent_yaml_path.relative_to(source))})
    
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The code has multiple instances where f-strings are used unnecessarily with static strings. This can be simplified for better readability and performance.
    4. cli/julep_cli/importt.py:181
    • Draft comment:
      Unnecessary use of f-string. Simplify to a regular string.
    add_entity_to_lock_file(type="agent", new_entity=LockedEntity(path=str(agent_yaml_path.relative_to(source)), id=remote_agent.id, last_synced=datetime.datetime.now().isoformat(timespec="milliseconds") + "Z", revision_hash=hashlib.sha256(agent_data.model_dump_json().encode()).hexdigest()), project_dir=source)
    
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The code has multiple instances where f-strings are used unnecessarily with static strings. This can be simplified for better readability and performance.

    Workflow ID: wflow_GGIwsFZ3Luc8MEIL


    You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

    Copy link

    gitguardian bot commented Jan 30, 2025

    ⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

    Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

    🔎 Detected hardcoded secret in your pull request
    GitGuardian id GitGuardian status Secret Commit Filename
    15378273 Triggered Generic High Entropy Secret 9b950d2 documentation/mint.json View secret
    🛠 Guidelines to remediate hardcoded secrets
    1. Understand the implications of revoking this secret by investigating where it is used in your code.
    2. Replace and store your secret safely. Learn here the best practices.
    3. Revoke and rotate this secret.
    4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

    To avoid such incidents in the future consider


    🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

    @HamadaSalhab HamadaSalhab merged commit d1fc444 into f/experimental-cli Jan 30, 2025
    6 of 8 checks passed
    @HamadaSalhab HamadaSalhab deleted the f/cli-rich-text branch January 30, 2025 01:38
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    1 participant