App-Greple-xlate
view release on metacpan or search on metacpan
lib/App/Greple/xlate/llm.pm view on Meta::CPAN
sub {
if (@before > 1) { pop @before; 1 }
elsif (@after > 1) { pop @after; 1 }
else { 0 }
},
sub {
if (length($sb) > $CONTEXT_SOURCE_MIN) {
$sb = substr($sb, -$CONTEXT_SOURCE_MIN);
$sb =~ s/\A[^\n]*\n//;
return 1;
}
if (length($sa) > $CONTEXT_SOURCE_MIN) {
$sa = substr($sa, 0, $CONTEXT_SOURCE_MIN);
$sa =~ s/(?<=\n)[^\n]*\z//;
return 1;
}
0;
},
sub {
if (@before) { pop @before; 1 }
elsif (@after) { pop @after; 1 }
else { 0 }
},
sub { $show_change ? do { $show_change = 0; 1 } : 0 },
sub { @old ? do { pop @old; 1 } : 0 },
);
my $data = $render->();
STEP: for my $step (@trim) {
while (length($json_flat->encode($data)) > $CONTEXT_MAX) {
$step->() or next STEP;
$data = $render->();
}
last;
}
$data;
}
sub build_request {
my $input = shift;
my $request = { input => $input };
if (my $context = context_payload()) {
$request->{context} = $context if %$context;
}
$request;
}
sub llm_command {
my($param, $system) = @_;
my @command = ('llm', '-m' => $param->{model}, '-s' => $system);
for my $kv (@{$param->{options} // []}) {
push @command, '-o', @$kv;
}
push @command, '--no-stream', '--no-log';
@command;
}
sub _llm_in_path {
grep { -x "$_/llm" } split /:/, $ENV{PATH} // '';
}
sub _not_found {
"llm: command not found.\n" .
"Install llm <https://llm.datasette.io/> with " .
"\"pip install llm\" or \"pipx install llm\".\n";
}
sub run_llm {
state $run = Command::Run->new;
my($param, $text) = @_;
##
## Check PATH before forking: Command::Run's forked child has no
## exit guard after a failed exec, so reaching that path would let
## the child escape into the caller's code.
##
_llm_in_path() or die _not_found();
my @command = llm_command($param, build_system($param));
warn Dumper \@command if opt('debug');
my $result = $run->command(@command)
->run(stdin => $text, stderr => 'capture');
if ($result->{result} != 0) {
die diagnose($param, $result);
}
print STDERR $result->{error} if $result->{error};
$result->{data};
}
##
## Called when the llm command fails: figure out why and return a
## message useful to the user.
##
sub diagnose {
my($param, $result) = @_;
my $stderr = $result->{error} // '';
if (! _llm_in_path()) {
return _not_found();
}
my $model = $param->{model};
my $models = Command::Run->new->command('llm', 'models')
->run(stderr => 'capture')->{data} // '';
if ($models !~ /\Q$model\E/) {
return "llm does not know model \"$model\".\n" .
"Upgrade llm (\"pip install -U llm\") or register the model " .
"in extra-openai-models.yaml.\n" .
($stderr ? "\n$stderr" : "");
}
return "llm failed:\n$stderr";
}
sub xlate_each {
my $param = shift;
my @count = map { int tr/\n/\n/ } @_;
_progress("From:\n", map s/^/\t< /mgr, @_);
my @in = map { m/.*\n/mg } @_;
my $out = run_llm($param, $json->encode(build_request(\@in)));
my $obj = eval { $json->decode($out) };
ref $obj eq 'ARRAY'
or die "Invalid JSON response:\n\n$out\n";
if (@$obj != @in) {
die sprintf("Unexpected response element count (%d != %d):\n\n%s\n",
scalar(@$obj), scalar(@in), $out);
}
for my $i (0 .. $#$obj) {
defined($obj->[$i]) && !ref($obj->[$i])
&& $json_flat->encode($obj->[$i]) =~ /\A"/
or die sprintf("Invalid response element %d (expected string):\n\n%s\n",
$i, $out);
}
my @out = map { s/(?<!\n)\z/\n/r } @$obj;
_progress("To:\n", map s/^/\t> /mgr, @out);
map { join '', splice @out, 0, $_ } @count;
}
##
## Public entry point for engine modules: batch the blocks up to the
## maxlen/maxline limits and translate each batch in one llm call.
##
sub xlate_with {
my $param = shift;
my @from = map { /\n\z/ ? $_ : "$_\n" } @_;
my @to;
my $max = $App::Greple::xlate::max_length || $param->{max} // die;
my $maxline = $App::Greple::xlate::max_line;
if (my @len = grep { $_ > $max } map length, @from) {
die "Contain lines longer than max length (@len > $max).\n";
}
while (@from) {
my @tmp;
my $len = 0;
while (@from) {
my $next = length $from[0];
last if $len + $next > $max;
$len += $next;
push @tmp, shift @from;
last if $maxline > 0 and @tmp >= $maxline;
}
( run in 2.701 seconds using v1.01-cache-2.11-cpan-a49fcb8fa48 )