Langertha

 view release on metacpan or  search on metacpan

lib/Langertha/Chat.pm  view on Meta::CPAN

  my ( $self, $data, $iteration ) = @_;
  for my $plugin (@{$self->_plugin_instances}) {
    $data = await $plugin->plugin_after_llm_response($data, $iteration);
  }
  return $data;
}

async sub _run_plugin_after_tool_call {
  my ( $self, $name, $input, $result ) = @_;
  for my $plugin (@{$self->_plugin_instances}) {
    $result = await $plugin->plugin_after_tool_call($name, $input, $result);
  }
  return $result;
}

# --- Simple chat (no tools) ---

sub simple_chat {
  my ( $self, @messages ) = @_;
  $log->debugf("[Chat] simple_chat via %s", ref $self->engine);
  my $engine = $self->_assert_chat_engine;
  my $conversation = $self->_build_messages(@messages);

  $conversation = $self->_run_plugin_before_llm_call($conversation, 1)->get;

  my $request = $engine->chat_request($conversation, $self->_extra);
  my $response = $engine->user_agent->request($request);
  my $data = $request->response_call->($response);

  $data = $self->_run_plugin_after_llm_response($data, 1)->get;

  return $data;
}


async sub simple_chat_f {
  my ( $self, @messages ) = @_;
  my $engine = $self->_assert_chat_engine;
  my $conversation = $self->_build_messages(@messages);

  $conversation = await $self->_run_plugin_before_llm_call($conversation, 1);

  my $request = $engine->chat_request($conversation, $self->_extra);
  my $response = await $engine->_async_http->do_request(
    request => $request,
  );
  unless ($response->is_success) {
    die "" . (ref $engine) . " request failed: " . $response->status_line;
  }
  my $data = $request->response_call->($response);

  $data = await $self->_run_plugin_after_llm_response($data, 1);

  return $data;
}


sub simple_chat_stream {
  my ( $self, $callback, @messages ) = @_;
  my $engine = $self->_assert_chat_engine;
  croak ref($engine) . " does not support streaming"
    unless $engine->can('chat_stream_request');
  croak "simple_chat_stream requires a callback as first argument"
    unless ref $callback eq 'CODE';
  my $conversation = $self->_build_messages(@messages);

  $conversation = $self->_run_plugin_before_llm_call($conversation, 1)->get;

  my $request = $engine->chat_stream_request($conversation, $self->_extra);
  my $chunks = $engine->execute_streaming_request($request, $callback);
  return join('', map { $_->content } @$chunks);
}


# --- Chat with tools ---

sub _gather_tools {
  my ( $self ) = @_;
  my @mcp_servers = @{$self->mcp_servers};
  croak "No MCP servers configured" unless @mcp_servers;

  my ( @all_tools, %tool_server_map );
  for my $mcp (@mcp_servers) {
    my $tools = $mcp->list_tools->get;
    for my $tool (@$tools) {
      $tool_server_map{$tool->{name}} = $mcp;
      push @all_tools, $tool;
    }
  }
  return (\@all_tools, \%tool_server_map);
}

sub _tool_loop_iteration {
  my ( $self, $engine, $conversation, $formatted_tools, $iteration ) = @_;

  # Plugin hook: before LLM call
  $conversation = $self->_run_plugin_before_llm_call($conversation, $iteration)->get;

  # Build and send the request
  my $request = $engine->build_tool_chat_request($conversation, $formatted_tools, $self->_extra);

  my $response = $engine->user_agent->request($request);
  my $data = ref $response eq 'HASH'
    ? $response  # mock: chat_request returns data directly via response_call
    : $engine->parse_response($response);

  # If response_call exists (Langertha::Request::HTTP), use it
  if (ref $request && $request->can('response_call') && $request->response_call) {
    $data = $request->response_call->($response);
  }

  # Plugin hook: after LLM response
  $data = $self->_run_plugin_after_llm_response($data, $iteration)->get;

  return ($conversation, $data);
}

sub simple_chat_with_tools {
  my ( $self, @messages ) = @_;
  my $engine = $self->_assert_chat_engine;
  croak ref($engine) . " does not support tools"
    unless $engine->does('Langertha::Role::Tools');

  my ($all_tools, $tool_server_map) = $self->_gather_tools;
  $log->debugf("[Chat] simple_chat_with_tools via %s, %d tools, max_iterations=%d",
    ref $engine, scalar @$all_tools, $self->tool_max_iterations);
  my $formatted_tools = $engine->format_tools($all_tools);
  my $conversation = $self->_build_messages(@messages);

  for my $iteration (1..$self->tool_max_iterations) {

lib/Langertha/Chat.pm  view on Meta::CPAN

        plugins     => ['Langfuse'],
    );
    my $result = $chat_tools->simple_chat_with_tools('List files in /tmp');

=head1 DESCRIPTION

C<Langertha::Chat> wraps any engine that consumes L<Langertha::Role::Chat>
and adds optional overrides for model, system prompt, and temperature, plus
plugin lifecycle hooks via L<Langertha::Role::PluginHost>.

Use this class when you want to share a single engine instance across
multiple chat contexts with different configurations, or when you need
plugin observability (e.g. L<Langertha::Plugin::Langfuse>) without
modifying the engine itself.

=head2 engine

The LLM engine to delegate chat requests to. Must consume
L<Langertha::Role::Chat>.

=head2 system_prompt

Optional system prompt. When set, prepended to messages for each
request, overriding any system prompt on the engine itself.

=head2 model

Optional model name override. When set, overrides the engine's
C<chat_model> via C<%extra> pass-through.

=head2 temperature

Optional temperature override. When set, overrides the engine's
temperature.

=head2 mcp_servers

ArrayRef of L<Net::Async::MCP> instances for tool calling.

=head2 tool_max_iterations

Maximum tool-calling round trips. Defaults to C<10>.

=head2 simple_chat

    my $response = $chat->simple_chat('Hello!');

Sends a synchronous chat request. Fires C<plugin_before_llm_call> and
C<plugin_after_llm_response> hooks.

=head2 simple_chat_f

    my $response = await $chat->simple_chat_f('Hello!');

Async version of L</simple_chat>.

=head2 simple_chat_stream

    my $content = $chat->simple_chat_stream(sub { print shift->content }, 'Hi');

Synchronous streaming chat. Calls C<$callback> with each chunk.

=head2 simple_chat_with_tools

    my $text = $chat->simple_chat_with_tools(@messages);

Synchronous tool-calling chat loop. Gathers tools from L</mcp_servers>,
sends chat requests, executes tool calls, and iterates until the LLM
returns a final text response. Fires plugin hooks at each step:
C<plugin_before_llm_call>, C<plugin_after_llm_response>,
C<plugin_before_tool_call>, and C<plugin_after_tool_call>.

=head2 simple_chat_with_tools_f

    my $text = await $chat->simple_chat_with_tools_f(@messages);

Async version of L</simple_chat_with_tools>.

=head1 SEE ALSO

=over

=item * L<Langertha::Role::PluginHost> - Plugin system consumed by this class

=item * L<Langertha::Role::Chat> - Chat role required by the engine

=item * L<Langertha::Role::Tools> - Tool-calling role required for MCP methods

=item * L<Langertha::Plugin::Langfuse> - Observability plugin for chat sessions

=item * L<Langertha::Embedder> - Embedding counterpart to this class

=item * L<Langertha::ImageGen> - Image generation counterpart to this class

=item * L<Langertha::Raider> - Autonomous agent with full conversation history

=back

=head1 SUPPORT

=head2 Issues

Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/langertha/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 <getty@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus L<https://raudssus.de/>.



( run in 1.064 second using v1.01-cache-2.11-cpan-0bb4e1dffa6 )