Langertha

 view release on metacpan or  search on metacpan

t/93_chat.t  view on Meta::CPAN

  has _async_http => (is => 'ro', lazy => 1, default => sub {
    require IO::Async::Loop;
    require Net::Async::HTTP;
    my $http = Net::Async::HTTP->new;
    IO::Async::Loop->new->add($http);
    return $http;
  });

  sub build_tool_chat_request {
    my ($self, $conversation, $formatted_tools, %extra) = @_;
    return $self->chat_request($conversation, tools => $formatted_tools, %extra);
  }

  sub format_tools {
    my ($self, $tools) = @_;
    return $tools;  # pass-through
  }

  sub response_tool_calls {
    my ($self, $data) = @_;
    return $data->{tool_calls} // [];
  }

  sub extract_tool_call {
    my ($self, $tc) = @_;
    return ($tc->{name}, $tc->{input} // {});
  }

  sub format_tool_results {
    my ($self, $data, $results) = @_;
    my @msgs;
    push @msgs, { role => 'assistant', content => 'called tools' };
    for my $r (@$results) {
      my $text = join('', map { $_->{text} // '' } @{$r->{result}{content} // []});
      push @msgs, { role => 'tool', content => $text };
    }
    return @msgs;
  }

  sub response_text_content {
    my ($self, $data) = @_;
    return $data->{final_text} // '';
  }

  sub parse_response {
    my ($self, $data) = @_;
    return $data;  # already parsed in our mock
  }

  sub think_tag_filter { 0 }
  sub json { JSON::MaybeXS->new(utf8 => 1) }

  __PACKAGE__->meta->make_immutable;
}

subtest 'Chat has tool-calling attributes' => sub {
  my $engine = MockChatEngine->new;
  my $chat = Langertha::Chat->new(engine => $engine);

  is_deeply($chat->mcp_servers, [], 'mcp_servers defaults to empty');
  is($chat->tool_max_iterations, 10, 'tool_max_iterations defaults to 10');
};

subtest 'simple_chat_with_tools_f dies without MCP servers' => sub {
  my $engine = MockToolChatEngine->new;
  my $chat = Langertha::Chat->new(engine => $engine);

  eval { $chat->simple_chat_with_tools_f('hello')->get };
  like($@, qr/No MCP servers/, 'dies without MCP servers');
};

subtest 'simple_chat_with_tools_f executes tool loop' => sub {
  my $mcp = MockMCPServer->new(
    get_time => sub { { content => [{ type => 'text', text => '12:00 PM' }] } },
  );

  my $engine = MockToolChatEngine->new(
    _response_queue => [
      # Iteration 1: LLM wants to call a tool
      {
        tool_calls => [{ name => 'get_time', input => {} }],
      },
      # Iteration 2: LLM returns final text
      {
        final_text => 'The time is 12:00 PM.',
      },
    ],
  );

  my $chat = Langertha::Chat->new(
    engine      => $engine,
    mcp_servers => [$mcp],
  );

  my $result = $chat->simple_chat_with_tools('What time is it?');
  is($result, 'The time is 12:00 PM.', 'got final response after tool loop');
};

subtest 'tool-calling fires plugin hooks' => sub {
  my $mcp = MockMCPServer->new(
    search => sub { { content => [{ type => 'text', text => 'found it' }] } },
  );

  # Logger that also logs tool calls
  {
    package ChatTestPlugin::ToolLogger;
    use Moose;
    use Future::AsyncAwait;
    extends 'Langertha::Plugin';

    has log => (is => 'ro', default => sub { [] });

    async sub plugin_before_llm_call {
      my ($self, $conv, $iter) = @_;
      push @{$self->log}, { event => 'before_llm', iteration => $iter };
      return $conv;
    }

    async sub plugin_after_llm_response {
      my ($self, $data, $iter) = @_;
      push @{$self->log}, { event => 'after_llm', iteration => $iter };



( run in 0.988 second using v1.01-cache-2.11-cpan-71847e10f99 )