Langertha-Knarr

 view release on metacpan or  search on metacpan

lib/Langertha/Knarr/PSGI.pm  view on Meta::CPAN

package Langertha::Knarr::PSGI;
# ABSTRACT: PSGI adapter for Langertha::Knarr (buffered, no streaming)
our $VERSION = '1.100';
use Moose;
use JSON::MaybeXS;
use Langertha::Knarr::Request;


# Wraps a Langertha::Knarr instance and returns a PSGI app coderef.
# Streaming requests are coerced into buffered responses: the full body is
# assembled (open + chunks + close + done) before being returned to the
# PSGI server. Use the native Net::Async::HTTP::Server entrypoint
# (Langertha::Knarr->run) if you need real streaming.

has knarr => ( is => 'ro', required => 1 );

has _json => (
  is => 'ro',
  default => sub { JSON::MaybeXS->new( utf8 => 1, canonical => 1 ) },
);

sub to_app {
  my ($self) = @_;
  return sub {
    my ($env) = @_;
    return $self->_handle_psgi($env);
  };
}

sub _read_body {
  my ($self, $env) = @_;
  my $input = $env->{'psgi.input'} or return '';
  my $len = $env->{CONTENT_LENGTH} // 0;
  return '' unless $len;
  my $body = '';
  my $read = 0;
  while ( $read < $len ) {
    my $chunk;
    my $n = $input->read( $chunk, $len - $read );
    last unless $n;
    $body .= $chunk;
    $read += $n;
  }
  return $body;
}

sub _handle_psgi {
  my ($self, $env) = @_;
  my $sb = $self->knarr;
  my $method = $env->{REQUEST_METHOD};
  my $path   = $env->{PATH_INFO} // '/';

  my $route = $sb->_match_route( $method, $path );
  unless ( $route ) {
    return [ 404, [ 'Content-Type' => 'application/json' ],
      [ $self->_json->encode({ error => { message => "no route for $method $path" } }) ] ];
  }

  my $proto = $route->{protocol};
  my $action = $route->{action};

  if ( $action eq 'models' || $action eq 'acp_agents' ) {
    my $models = $sb->handler->list_models;
    my ($status, $headers, $body) = $proto->format_models_response($models);
    return [ $status, [ %$headers ], [ $body ] ];
  }
  if ( $action eq 'a2a_card' ) {
    my ($status, $headers, $body) = $proto->format_agent_card;
    return [ $status, [ %$headers ], [ $body ] ];
  }
  if ( $action ne 'chat' ) {
    return [ 500, [ 'Content-Type' => 'application/json' ],
      [ $self->_json->encode({ error => { message => "unknown action $action" } }) ] ];
  }

  my $body = $self->_read_body($env);
  my $fake_http = Langertha::Knarr::PSGI::FakeReq->new( $env );
  my $sb_req = $proto->parse_chat_request( $fake_http, \$body );
  my $session = $sb->session( $sb_req->session_id );
  my $handler = $sb->handler;

  if ( $sb_req->stream ) {
    # Buffered streaming: drive the stream to completion, concatenate frames.
    my $stream = $handler->handle_stream_f( $session, $sb_req )->get;
    my $out = $proto->format_stream_open($sb_req);
    while ( defined( my $delta = $stream->next_chunk_f->get ) ) {
      $out .= $proto->format_stream_chunk( $delta, $sb_req );
    }
    $out .= $proto->format_stream_close($sb_req);
    $out .= $proto->format_stream_done($sb_req);
    return [ 200, [ 'Content-Type' => $proto->stream_content_type ], [ $out ] ];
  }

  my $response = $handler->handle_chat_f( $session, $sb_req )->get;
  my ($status, $headers, $obody) = $proto->format_chat_response( $response, $sb_req );
  return [ $status, [ %$headers ], [ $obody ] ];
}

package Langertha::Knarr::PSGI::FakeReq;
sub new {
  my ($class, $env) = @_;
  return bless { env => $env }, $class;
}
sub header {
  my ($self, $name) = @_;
  ( my $key = uc $name ) =~ tr/-/_/;
  return $self->{env}{"HTTP_$key"};
}

package Langertha::Knarr::PSGI;
__PACKAGE__->meta->make_immutable;
1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Langertha::Knarr::PSGI - PSGI adapter for Langertha::Knarr (buffered, no streaming)

=head1 VERSION

version 1.100

=head1 SYNOPSIS

    use Langertha::Knarr;
    use Langertha::Knarr::PSGI;

    my $knarr = Langertha::Knarr->new( handler => $handler );
    my $app = Langertha::Knarr::PSGI->new( knarr => $knarr )->to_app;
    # $app is now a Plack-compatible coderef

    # Run with any PSGI server:
    #   plackup -s Starman -p 8088 app.psgi

=head1 DESCRIPTION

Adapter that wraps a L<Langertha::Knarr> instance and exposes it as a
PSGI app, so you can deploy Knarr behind any Plack server (Starman,
Twiggy, Gazelle, mod_perl, etc.) instead of running its native
L<Net::Async::HTTP::Server> loop.

B<Streaming responses are buffered.> The PSGI streaming protocol's
delayed-response form does work in theory but mixes badly with
L<IO::Async> in the same process; for honesty's sake this adapter
just drives the inner stream to completion in a blocking loop and
returns the full assembled body. Use the native
L<Langertha::Knarr/run> entry point if you need real-time streaming.

=head2 knarr

Required. The L<Langertha::Knarr> instance to expose.

=head2 to_app

Returns the PSGI coderef.

=head1 SUPPORT

=head2 Issues

Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/langertha-knarr/issues>.

=head2 IRC

Join C<#langertha> on C<irc.perl.org> or message Getty directly.

=head1 CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

=head1 AUTHOR

Torsten Raudssus <torsten@raudssus.de> L<https://raudssus.de/>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut



( run in 0.462 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )