Developer-Dashboard

 view release on metacpan or  search on metacpan

lib/Developer/Dashboard/CLI/Ask.pm  view on Meta::CPAN

# Output: backend name string.
sub _resolve_backend {
    my ( $opts, $transcript ) = @_;
    return $opts->{backend} if $opts->{backend} ne '';
    return $transcript->{backend} if $transcript->{backend} && $BACKEND_FLAG{ $transcript->{backend} };
    return 'claude';
}

# _dispatch_backend(%args)
# Routes one turn to the selected backend and returns its answer text.
# Input: backend, prompt, model, images/text_files array refs, history array
# ref, claude_conf hash ref, env, and the ua/runner/detect seams.
# Output: answer text string; dies on backend failure.
sub _dispatch_backend {
    my (%a) = @_;
    return _ask_claude(%a) if $a{backend} eq 'claude';
    return _ask_nova(%a) if $a{backend} eq 'nova';
    return _ask_cli_backend(%a);
}

# _ask_claude(%args)
# Answers via the Anthropic API when a key is resolvable, else falls back to the
# local `claude` CLI (Claude Code).
# Input: same payload as _dispatch_backend.
# Output: answer text string; dies when no key and no CLI are available.
sub _ask_claude {
    my (%a) = @_;
    my $key = _resolve_api_key( $a{claude_conf}, $a{env} );
    if ( $key ne '' ) {
        my $messages = _build_api_messages( $a{history}, $a{prompt}, $a{text_files}, $a{images} );
        my $ua = $a{ua};
        $ua = _default_ua() if !$ua;
        my $base_url = $a{claude_conf}{base_url};
        $base_url = $DEFAULT_BASE_URL if !$base_url;
        my $model = $a{model};
        $model = $DEFAULT_MODEL if !$model;
        my $max_tokens = $a{claude_conf}{max_tokens};
        $max_tokens = $DEFAULT_MAX_TOKENS if !$max_tokens;
        return _call_claude_api(
            ua         => $ua,
            key        => $key,
            base_url   => $base_url,
            model      => $model,
            max_tokens => $max_tokens,
            messages   => $messages,
            root       => $a{paths}->current_project_root,
        );
    }

    die "Image attachments need an ANTHROPIC_API_KEY; the local claude CLI fallback cannot attach images.\n"
      if @{ $a{images} };
    my $cli = $a{detect}->('claude')
      or die "No ANTHROPIC_API_KEY set and no `claude` CLI found. Set the key or install Claude Code.\n";
    my $prompt = _compose_cli_prompt( $a{history}, $a{prompt}, $a{text_files} );
    my @argv = ( command_argv_for_path($cli), '-p', $prompt, '--output-format', 'text' );
    push @argv, ( '--model', $a{model} ) if defined $a{model};
    return _capture_backend( 'claude', \@argv, $a{runner} );
}

# _ask_nova(%args)
# Answers via Amazon Nova's own standalone REST endpoint (DD-952) -
# api.nova.amazon.com/v1/chat/completions, bearer-token auth. This is
# architecturally identical to _ask_claude's direct-API path (see
# docs/dashboard-ask-backend-architecture.md), never AWS Bedrock/SigV4 -
# NOVA_API_KEY is a plain bearer token, not an AWS credential.
# Input: same payload as _dispatch_backend.
# Output: answer text string; dies when no NOVA_API_KEY is set or images
# are attached (Nova's image content-block shape is not implemented here).
sub _ask_nova {
    my (%a) = @_;
    my $key = $a{env}{NOVA_API_KEY};
    die "No NOVA_API_KEY set. Set it to use --nova.\n" if !defined $key || $key eq '';
    die "Image attachments are not supported with --nova.\n" if @{ $a{images} };
    my $messages = _build_api_messages( $a{history}, $a{prompt}, $a{text_files}, [] );
    my $ua = $a{ua};
    $ua = _default_ua() if !$ua;
    my $model = $a{model};
    $model = $NOVA_DEFAULT_MODEL if !$model;
    return _call_nova_api(
        ua       => $ua,
        key      => $key,
        base_url => $NOVA_DEFAULT_BASE_URL,
        model    => $model,
        messages => $messages,
    );
}

# _call_nova_api(%args)
# Posts one request to Nova's chat-completions endpoint and extracts the
# answer text.
# Input: ua, key, base_url, model, and messages array ref.
# Output: answer text string; dies on a non-success HTTP response or an
# unparseable/empty response body.
sub _call_nova_api {
    my (%a) = @_;
    require HTTP::Request;
    my $url = $a{base_url} . '/v1/chat/completions';
    my $req = HTTP::Request->new( POST => $url );
    $req->header( 'content-type'  => 'application/json' );
    $req->header( 'authorization' => "Bearer $a{key}" );
    $req->content(
        json_encode(
            {
                model    => $a{model},
                messages => $a{messages},
            }
        )
    );

    my $resp = $a{ua}->request($req);
    die "Nova API request failed: @{[ $resp->status_line ]}\n" if !$resp->is_success;
    return _extract_nova_api_text( json_decode( $resp->decoded_content ) );
}

# _extract_nova_api_text($data)
# Extracts the answer text from a Nova chat-completions response
# (OpenAI-chat-completions shape: {choices:[{message:{content}}]}), a
# genuinely different response body than Claude's {content:[...]} shape.
# Input: decoded response hash ref.
# Output: answer text string; dies when no text content is present.
sub _extract_nova_api_text {
    my ($data) = @_;
    die "Nova API returned no content.\n"
      if ref($data) ne 'HASH' || ref( $data->{choices} ) ne 'ARRAY' || !@{ $data->{choices} };
    my $content = $data->{choices}[0]{message}{content};
    die "Nova API returned no text.\n" if !defined $content || $content eq '';
    return $content;
}

# _ask_cli_backend(%args)
# Answers via a shelled-out CLI backend (codex/copilot/gemini), forcing a
# read-only, non-interactive invocation and attaching images natively.
# Input: same payload as _dispatch_backend.
# Output: answer text string; dies when the backend CLI is missing or fails.
sub _ask_cli_backend {
    my (%a) = @_;
    my $name = $a{backend};
    my $cli  = $a{detect}->($name)
      or die _missing_backend_message($name);

    my $prompt = _compose_cli_prompt( $a{history}, $a{prompt}, $a{text_files} );
    my @base = command_argv_for_path($cli);
    my @argv;
    if ( $name eq 'codex' ) {
        @argv = ( @base, 'exec', '-s', 'read-only', '--skip-git-repo-check', '--color', 'never' );
        push @argv, ( '--model', $a{model} ) if defined $a{model};
        push @argv, ( '-i', $_ ) for @{ $a{images} };
        push @argv, ( '--', $prompt );
    }



( run in 0.745 second using v1.01-cache-2.11-cpan-16aabcbfbcb )