Skip to content

Commit

Permalink
LLM C++ test and doc updates (#1379)
Browse files Browse the repository at this point in the history
- C++ unit test updates
- Split some tests into multiple tests
- Minor doc updates and fixes

## By Submitting this PR I confirm:
- I am familiar with the [Contributing Guidelines](https://github.com/nv-morpheus/Morpheus/blob/main/docs/source/developer_guide/contributing.md).
- When the PR is ready for review, new or existing tests cover these changes.
- When the PR is ready for review, the documentation is up to date with these changes.

Authors:
  - Eli Fajardo (https://github.com/efajardo-nv)
  - Michael Demoret (https://github.com/mdemoret-nv)

Approvers:
  - Michael Demoret (https://github.com/mdemoret-nv)

URL: #1379
  • Loading branch information
efajardo-nv authored Nov 23, 2023
1 parent d2a962d commit f87f16c
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 106 deletions.
2 changes: 1 addition & 1 deletion morpheus/_lib/include/morpheus/llm/llm_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class MORPHEUS_EXPORT LLMContext : public std::enable_shared_from_this<LLMContex
std::string full_name() const;

/**
* @brief Create new context from this context with provided name and input mappings.
* @brief Create new context from this context using provided name and input mappings.
*
* @param name name of new context
* @param inputs input mappings for new context
Expand Down
6 changes: 3 additions & 3 deletions morpheus/_lib/include/morpheus/llm/llm_engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ namespace morpheus::llm {
/**
* @brief A subclass of LLMNode that acts as container for components required in processing LLM service
* requests. For example, a prompt generator and LLM service can each be implemented as an LLMNode and added to
* the engine. Input mappings are used to match node's input to outputs of parent of sibling nodes. Task handlers
* can also be added to the engine to process the outputs of an LLM service to feed back to prompt generator, for
* example, or exit the engine.
* the engine. Input mappings are used to match node's input to outputs of parent or sibling nodes. Task handlers
* can also be added to the engine to perform additional processing on the outputs stored in the LLMContext of the
* engine.
*/
class MORPHEUS_EXPORT LLMEngine : public LLMNode
{
Expand Down
2 changes: 1 addition & 1 deletion morpheus/_lib/include/morpheus/llm/llm_task.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace morpheus::llm {

/**
* @brief Holds information about LLM task. This information is extracted by LLMEngine from
* input control message and saved to context for use by task handler(s).
* input control message and saved to the context for use by task handler(s).
*/
struct MORPHEUS_EXPORT LLMTask
{
Expand Down
2 changes: 1 addition & 1 deletion morpheus/_lib/include/morpheus/llm/utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ bool MORPHEUS_EXPORT is_valid_node_name(std::string_view name);

/**
* @brief Resolves user input mappings to final mappings. Handles replacement of placeholders in inputs with
* with actual internal input names provided in input_names.
* actual internal input names provided in input_names.
*
* @param user_inputs user input mappings
* @param input_names internal input names for context
Expand Down
188 changes: 147 additions & 41 deletions morpheus/_lib/tests/llm/test_llm_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,101 @@ TEST_F(TestLLMContext, Initialization)
ASSERT_EQ(ctx_5.full_name(), "/child");
}

TEST_F(TestLLMContext, InitWithLLMTask)
{
nlohmann::json task_dict;
task_dict = {
{"task_type", "dictionary"},
{"model_name", "test"},
};

llm::LLMContext ctx{llm::LLMTask{"template", task_dict}, nullptr};
ASSERT_EQ(ctx.task().get("task_type"), "dictionary");
ASSERT_EQ(ctx.task().get("model_name"), "test");
}

TEST_F(TestLLMContext, InitWithControlMessageTask)
{
nlohmann::json task_dict;
task_dict = {
{"task_type", "dictionary"},
{"model_name", "test"},
};

nlohmann::json msg_config;
msg_config["tasks"] = {{{"type", "llm_engine"}, {"properties", {{"type", "template"}, {"properties", task_dict}}}}};

auto msg = std::make_shared<ControlMessage>(msg_config);

llm::LLMContext ctx{llm::LLMTask{}, msg};
ASSERT_EQ(ctx.message()->has_task("llm_engine"), true);
}

TEST_F(TestLLMContext, InitWithLLMTaskAndControlMessageTask)
{
nlohmann::json task_dict;
task_dict = {
{"task_type", "dictionary"},
{"model_name", "test"},
};

nlohmann::json msg_config;
msg_config["tasks"] = {{{"type", "llm_engine"}, {"properties", {{"type", "template"}, {"properties", task_dict}}}}};

auto msg = std::make_shared<ControlMessage>(msg_config);

llm::LLMContext ctx{llm::LLMTask{"template", task_dict}, msg};
ASSERT_EQ(ctx.message()->has_task("llm_engine"), true);
ASSERT_EQ(ctx.task().get("task_type"), "dictionary");
ASSERT_EQ(ctx.task().get("model_name"), "test");
}

TEST_F(TestLLMContext, InitWithParentContext)
{
nlohmann::json task_dict;
task_dict = {
{"task_type", "dictionary"},
{"model_name", "test"},
};

nlohmann::json msg_config;
msg_config["tasks"] = {{{"type", "llm_engine"}, {"properties", {{"type", "template"}, {"properties", task_dict}}}}};

auto msg = std::make_shared<ControlMessage>(msg_config);

auto parent_ctx = std::make_shared<llm::LLMContext>(llm::LLMTask{"template", task_dict}, msg);
auto inputs = llm::input_mappings_t{{"/ext1", "input1"}};
llm::LLMContext ctx{parent_ctx, "child", inputs};
ASSERT_EQ(ctx.input_map()[0].external_name, "/ext1");
ASSERT_EQ(ctx.input_map()[0].internal_name, "input1");
ASSERT_EQ(ctx.parent()->message()->has_task("llm_engine"), true);
ASSERT_EQ(ctx.parent()->task().get("task_type"), "dictionary");
ASSERT_EQ(ctx.parent()->task().get("model_name"), "test");
ASSERT_EQ(ctx.name(), "child");
ASSERT_EQ(ctx.full_name(), "/child");
}

TEST_F(TestLLMContext, SetOutput)
{
llm::LLMContext ctx_1{llm::LLMTask{}, nullptr};
llm::LLMContext ctx{llm::LLMTask{}, nullptr};
nlohmann::json outputs;
outputs = {{"key1", "val1"}, {"key2", "val2"}};
ctx_1.set_output(outputs);
ctx.set_output(outputs);

ASSERT_EQ(ctx.all_outputs().size(), 2);
ASSERT_EQ(ctx.all_outputs()["key1"], "val1");
ASSERT_EQ(ctx.all_outputs()["key2"], "val2");
}

ASSERT_EQ(ctx_1.all_outputs()["key1"], "val1");
ASSERT_EQ(ctx_1.all_outputs()["key2"], "val2");
TEST_F(TestLLMContext, SetOutputDict)
{
llm::LLMContext ctx{llm::LLMTask{}, nullptr};
nlohmann::json outputs;
outputs = {{"key1", "val1"}, {"key2", "val2"}};

llm::LLMContext ctx_2{llm::LLMTask{}, nullptr};
ctx_2.set_output("output", outputs);
ASSERT_EQ(ctx_2.all_outputs()["output"]["key1"], "val1");
ASSERT_EQ(ctx_2.all_outputs()["output"]["key2"], "val2");
ctx.set_output("output", outputs);
ASSERT_EQ(ctx.all_outputs()["output"]["key1"], "val1");
ASSERT_EQ(ctx.all_outputs()["output"]["key2"], "val2");
}

TEST_F(TestLLMContext, PushPop)
Expand Down Expand Up @@ -188,51 +269,76 @@ TEST_F(TestLLMContext, PopSelectMultipleOutputs)
ASSERT_EQ(child_ctx->parent()->all_outputs()["child"]["key3"], "val3");
}

TEST_F(TestLLMContext, SingleInputMapping)
TEST_F(TestLLMContext, SingleInputMappingValid)
{
auto parent_ctx = std::make_shared<llm::LLMContext>(llm::LLMTask{}, nullptr);
nlohmann::json outputs;
outputs = {{"parent_out", "val1"}};
parent_ctx->set_output(outputs);

auto inputs_1 = llm::input_mappings_t{{"/parent_out", "input1"}};
llm::LLMContext child_ctx_1{parent_ctx, "child", inputs_1};
ASSERT_EQ(child_ctx_1.get_input(), "val1");
ASSERT_EQ(child_ctx_1.get_input("input1"), "val1");
ASSERT_EQ(child_ctx_1.get_inputs()["input1"], "val1");
ASSERT_THROW(child_ctx_1.get_input("input2"), std::runtime_error);

auto inputs_2 = llm::input_mappings_t{{"/invalid", "input1"}};
llm::LLMContext child_ctx_2{parent_ctx, "child", inputs_2};
ASSERT_THROW(child_ctx_2.get_input(), std::runtime_error);
ASSERT_THROW(child_ctx_2.get_input("input1"), std::runtime_error);
ASSERT_THROW(child_ctx_2.get_inputs()["input1"], std::runtime_error);
auto inputs = llm::input_mappings_t{{"/parent_out", "input1"}};
llm::LLMContext child_ctx{parent_ctx, "child", inputs};
ASSERT_EQ(child_ctx.get_input(), "val1");
ASSERT_EQ(child_ctx.get_input("input1"), "val1");
ASSERT_EQ(child_ctx.get_inputs()["input1"], "val1");
ASSERT_THROW(child_ctx.get_input("input2"), std::runtime_error);
}

TEST_F(TestLLMContext, MultipleInputMappings)
TEST_F(TestLLMContext, SingleInputMappingInvalid)
{
auto parent_ctx = std::make_shared<llm::LLMContext>(llm::LLMTask{}, nullptr);
nlohmann::json outputs;
outputs = {{"parent_out", "val1"}};
parent_ctx->set_output(outputs);

auto inputs = llm::input_mappings_t{{"/invalid", "input1"}};
llm::LLMContext child_ctx{parent_ctx, "child", inputs};
ASSERT_THROW(child_ctx.get_input(), std::runtime_error);
ASSERT_THROW(child_ctx.get_input("input1"), std::runtime_error);
ASSERT_THROW(child_ctx.get_inputs()["input1"], std::runtime_error);
}

TEST_F(TestLLMContext, MultipleInputMappingsValid)
{
auto parent_ctx = std::make_shared<llm::LLMContext>(llm::LLMTask{}, nullptr);
nlohmann::json outputs;
outputs = {{"parent_out1", "val1"}, {"parent_out2", "val2"}};
parent_ctx->set_output(outputs);

auto inputs_1 = llm::input_mappings_t{{"/parent_out1", "input1"}, {"/parent_out2", "input2"}};
llm::LLMContext child_ctx_1{parent_ctx, "child", inputs_1};
ASSERT_EQ(child_ctx_1.get_input("input1"), "val1");
ASSERT_EQ(child_ctx_1.get_input("input2"), "val2");
ASSERT_EQ(child_ctx_1.get_inputs()["input1"], "val1");
ASSERT_EQ(child_ctx_1.get_inputs()["input2"], "val2");
ASSERT_THROW(child_ctx_1.get_input(), std::runtime_error);
ASSERT_THROW(child_ctx_1.get_input("input3"), std::runtime_error);

auto inputs_2 = llm::input_mappings_t{{"/invalid", "input1"}};
llm::LLMContext child_ctx_2{parent_ctx, "child", inputs_2};
ASSERT_THROW(child_ctx_2.get_input(), std::runtime_error);
ASSERT_THROW(child_ctx_2.get_input("input1"), std::runtime_error);
ASSERT_THROW(child_ctx_2.get_inputs()["input1"], std::runtime_error);

auto inputs_3 = llm::input_mappings_t{{"/parent_out1", "input1"}, {"/invalid", "input2"}};
llm::LLMContext child_ctx_3{parent_ctx, "child", inputs_2};
ASSERT_EQ(child_ctx_1.get_input("input1"), "val1");
ASSERT_THROW(child_ctx_2.get_input("input2"), std::runtime_error);
auto inputs = llm::input_mappings_t{{"/parent_out1", "input1"}, {"/parent_out2", "input2"}};
llm::LLMContext child_ctx{parent_ctx, "child", inputs};
ASSERT_EQ(child_ctx.get_input("input1"), "val1");
ASSERT_EQ(child_ctx.get_input("input2"), "val2");
ASSERT_EQ(child_ctx.get_inputs()["input1"], "val1");
ASSERT_EQ(child_ctx.get_inputs()["input2"], "val2");
ASSERT_THROW(child_ctx.get_input(), std::runtime_error);
ASSERT_THROW(child_ctx.get_input("input3"), std::runtime_error);
}

TEST_F(TestLLMContext, MultipleInputMappingsSingleInvalid)
{
auto parent_ctx = std::make_shared<llm::LLMContext>(llm::LLMTask{}, nullptr);
nlohmann::json outputs;
outputs = {{"parent_out1", "val1"}, {"parent_out2", "val2"}};
parent_ctx->set_output(outputs);

auto inputs = llm::input_mappings_t{{"/parent_out1", "input1"}, {"/invalid", "input2"}};
llm::LLMContext child_ctx{parent_ctx, "child", inputs};
ASSERT_EQ(child_ctx.get_input("input1"), "val1");
ASSERT_THROW(child_ctx.get_input("input2"), std::runtime_error);
ASSERT_THROW(child_ctx.get_inputs(), std::runtime_error);
}

TEST_F(TestLLMContext, MultipleInputMappingsBothInvalid)
{
auto parent_ctx = std::make_shared<llm::LLMContext>(llm::LLMTask{}, nullptr);
nlohmann::json outputs;
outputs = {{"parent_out1", "val1"}, {"parent_out2", "val2"}};
parent_ctx->set_output(outputs);

auto inputs = llm::input_mappings_t{{"/invalid1", "input1"}, {"/invalid2", "input2"}};
llm::LLMContext child_ctx{parent_ctx, "child", inputs};
ASSERT_THROW(child_ctx.get_input("input1"), std::runtime_error);
ASSERT_THROW(child_ctx.get_input("input2"), std::runtime_error);
ASSERT_THROW(child_ctx.get_inputs(), std::runtime_error);
}
Loading

0 comments on commit f87f16c

Please sign in to comment.