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 )