From ec435df4ad4c0c7118973b5795fa097f3f3c01ed Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Fri, 1 May 2026 11:14:49 -0400 Subject: [PATCH] add prompt dir app option --- tooling/pipeline/CMakeLists.txt | 2 +- .../data_generation/llama_generator.h | 16 ++-- .../includes/data_model/application_options.h | 4 + .../includes/services/prompt_directory.h | 76 ++++++++++++++++ ...en_seed_2026-05-01T15-12-28.300376Z.sqlite | Bin 0 -> 40960 bytes .../data_generation/llama/generate_brewery.cc | 7 +- .../data_generation/llama/llama_generator.cc | 11 ++- .../llama/load_brewery_prompt.cc | 55 ------------ tooling/pipeline/src/main.cc | 37 +++++++- .../pipeline/src/services/prompt_directory.cc | 85 ++++++++++++++++++ 10 files changed, 218 insertions(+), 75 deletions(-) create mode 100644 tooling/pipeline/includes/services/prompt_directory.h create mode 100644 tooling/pipeline/output/biergarten_seed_2026-05-01T15-12-28.300376Z.sqlite delete mode 100644 tooling/pipeline/src/data_generation/llama/load_brewery_prompt.cc create mode 100644 tooling/pipeline/src/services/prompt_directory.cc diff --git a/tooling/pipeline/CMakeLists.txt b/tooling/pipeline/CMakeLists.txt index 24628fd..2985138 100644 --- a/tooling/pipeline/CMakeLists.txt +++ b/tooling/pipeline/CMakeLists.txt @@ -133,7 +133,7 @@ set(SOURCES src/data_generation/llama/helpers.cc src/data_generation/llama/infer.cc src/data_generation/llama/load.cc - src/data_generation/llama/load_brewery_prompt.cc + src/services/prompt_directory.cc src/data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.cc src/data_generation/mock/deterministic_hash.cc src/data_generation/mock/generate_brewery.cc diff --git a/tooling/pipeline/includes/data_generation/llama_generator.h b/tooling/pipeline/includes/data_generation/llama_generator.h index 1e648a3..7479337 100644 --- a/tooling/pipeline/includes/data_generation/llama_generator.h +++ b/tooling/pipeline/includes/data_generation/llama_generator.h @@ -17,6 +17,7 @@ #include "data_generation/data_generator.h" #include "data_generation/prompt_formatting/prompt_formatter.h" #include "data_model/application_options.h" +#include "services/prompt_directory.h" struct llama_model; struct llama_context; @@ -33,10 +34,12 @@ class LlamaGenerator final : public DataGenerator { * @param options Parsed application options. * @param model_path Filesystem path to GGUF model assets. * @param prompt_formatter Formatter that produces model-specific prompts. + * @param prompt_directory Directory service for loading named prompt files. */ LlamaGenerator(const ApplicationOptions& options, const std::string& model_path, - std::unique_ptr prompt_formatter); + std::unique_ptr prompt_formatter, + std::unique_ptr prompt_directory); ~LlamaGenerator() override; @@ -119,15 +122,6 @@ class LlamaGenerator final : public DataGenerator { int max_tokens = kDefaultMaxTokens, std::string_view grammar = {}); - /** - * @brief Loads the brewery system prompt from disk. - * - * @param prompt_file_path Prompt file path to try first. - * @return Loaded prompt text. - */ - std::string LoadBrewerySystemPrompt( - const std::filesystem::path& prompt_file_path); - ModelHandle model_; ContextHandle context_; float sampling_temperature_ = 1.0F; @@ -135,8 +129,8 @@ class LlamaGenerator final : public DataGenerator { uint32_t sampling_top_k_ = kDefaultSamplingTopK; std::mt19937 rng_; uint32_t n_ctx_ = kDefaultContextSize; - std::string brewery_system_prompt_; std::unique_ptr prompt_formatter_; + std::unique_ptr prompt_directory_; }; #endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_LLAMA_GENERATOR_H_ diff --git a/tooling/pipeline/includes/data_model/application_options.h b/tooling/pipeline/includes/data_model/application_options.h index 08b7d8c..5893ec8 100644 --- a/tooling/pipeline/includes/data_model/application_options.h +++ b/tooling/pipeline/includes/data_model/application_options.h @@ -53,6 +53,10 @@ struct PipelineOptions { /// @brief Directory for generated artifacts. std::filesystem::path output_path; + /// @brief Directory that contains named prompt files (e.g. + /// BREWERY_GENERATION.md). + std::filesystem::path prompt_dir; + /// @brief Path for application logs. std::filesystem::path log_path; }; diff --git a/tooling/pipeline/includes/services/prompt_directory.h b/tooling/pipeline/includes/services/prompt_directory.h new file mode 100644 index 0000000..53410ad --- /dev/null +++ b/tooling/pipeline/includes/services/prompt_directory.h @@ -0,0 +1,76 @@ +#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_PROMPT_DIRECTORY_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_PROMPT_DIRECTORY_H_ + +/** + * @file services/prompt_directory.h + * @brief Interface and filesystem-backed implementation for named prompt + * loading. + * + * Prompt files are resolved by key: a key of "BREWERY_GENERATION" maps to the + * file /BREWERY_GENERATION.md. The interface is kept intentionally + * narrow so test doubles can be injected without touching the filesystem. + */ + +#include +#include +#include +#include +#include + +/** + * @brief Interface for loading named prompt files. + */ +class IPromptDirectory { + public: + IPromptDirectory() = default; + IPromptDirectory(const IPromptDirectory&) = delete; + IPromptDirectory& operator=(const IPromptDirectory&) = delete; + IPromptDirectory(IPromptDirectory&&) = delete; + IPromptDirectory& operator=(IPromptDirectory&&) = delete; + virtual ~IPromptDirectory() = default; + + /** + * @brief Loads the prompt associated with @p key. + * + * @param key Logical prompt key, e.g. "BREWERY_GENERATION". + * @return Prompt text. + * @throws std::runtime_error if the prompt file cannot be found or read. + */ + [[nodiscard]] virtual std::string Load(std::string_view key) = 0; +}; + +/** + * @brief Filesystem-backed IPromptDirectory implementation. + * + * Each call to Load() checks an in-process cache first, then reads + * /.md from disk. The directory must exist and be readable + * at construction time; individual file absence is reported lazily at Load(). + */ +class PromptDirectory final : public IPromptDirectory { + public: + /** + * @brief Constructs a PromptDirectory rooted at @p prompt_dir. + * + * @param prompt_dir Absolute or relative path to the prompt directory. + * @throws std::runtime_error if @p prompt_dir does not exist or is not a + * directory. + */ + explicit PromptDirectory(const std::filesystem::path& prompt_dir); + + /** + * @brief Loads the prompt for @p key, caching the result. + * + * Maps @p key → /.md. + * + * @param key Logical prompt key. + * @return Prompt text. + * @throws std::runtime_error if the file does not exist or is empty. + */ + [[nodiscard]] std::string Load(std::string_view key) override; + + private: + std::filesystem::path prompt_dir_; + std::unordered_map cache_; +}; + +#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_PROMPT_DIRECTORY_H_ diff --git a/tooling/pipeline/output/biergarten_seed_2026-05-01T15-12-28.300376Z.sqlite b/tooling/pipeline/output/biergarten_seed_2026-05-01T15-12-28.300376Z.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..8dbf1c1cbedb86bc0f910f6c2d02674ae08264fb GIT binary patch literal 40960 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 Rh}r^1sO%PGd}qlu`7aj)YX|@U literal 0 HcmV?d00001 diff --git a/tooling/pipeline/src/data_generation/llama/generate_brewery.cc b/tooling/pipeline/src/data_generation/llama/generate_brewery.cc index 7e92664..f29c637 100644 --- a/tooling/pipeline/src/data_generation/llama/generate_brewery.cc +++ b/tooling/pipeline/src/data_generation/llama/generate_brewery.cc @@ -59,11 +59,12 @@ BreweryResult LlamaGenerator::GenerateBrewery( location.country.empty() ? std::string{} : std::format(", {}", location.country); /** - * Load brewery system prompt from file - * Falls back to minimal inline prompt if file not found + * Load brewery system prompt via the injected prompt directory. + * The key "BREWERY_GENERATION" resolves to BREWERY_GENERATION.md inside + * the configured --prompt-dir. Throws on missing or empty file. */ const std::string system_prompt = - LoadBrewerySystemPrompt("prompts/system.md"); + prompt_directory_->Load("BREWERY_GENERATION"); std::string user_prompt = std::format( "## CITY:\n{}\n\n## COUNTRY:\n{}\n\n## LOCAL LANGUAGE CODES:\n{}\n\n## " diff --git a/tooling/pipeline/src/data_generation/llama/llama_generator.cc b/tooling/pipeline/src/data_generation/llama/llama_generator.cc index 5f28b1b..2a12dc8 100644 --- a/tooling/pipeline/src/data_generation/llama/llama_generator.cc +++ b/tooling/pipeline/src/data_generation/llama/llama_generator.cc @@ -32,9 +32,11 @@ void LlamaGenerator::ContextDeleter::operator()( LlamaGenerator::LlamaGenerator( const ApplicationOptions& options, const std::string& model_path, - std::unique_ptr prompt_formatter) + std::unique_ptr prompt_formatter, + std::unique_ptr prompt_directory) : rng_(std::random_device{}()), - prompt_formatter_(std::move(prompt_formatter)) { + prompt_formatter_(std::move(prompt_formatter)), + prompt_directory_(std::move(prompt_directory)) { if (model_path.empty()) { throw std::runtime_error("LlamaGenerator: model path must not be empty"); } @@ -44,6 +46,11 @@ LlamaGenerator::LlamaGenerator( "LlamaGenerator: prompt formatter dependency must not be null"); } + if (!prompt_directory_) { + throw std::runtime_error( + "LlamaGenerator: prompt directory dependency must not be null"); + } + const auto sampling = options.generator.sampling.value_or(SamplingOptions{}); if (sampling.temperature < 0.0F) { diff --git a/tooling/pipeline/src/data_generation/llama/load_brewery_prompt.cc b/tooling/pipeline/src/data_generation/llama/load_brewery_prompt.cc deleted file mode 100644 index f59d590..0000000 --- a/tooling/pipeline/src/data_generation/llama/load_brewery_prompt.cc +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @file data_generation/llama/load_brewery_prompt.cc - * @brief Resolves brewery system prompt content from cache or a configured - * filesystem path and provides a robust inline fallback prompt when absent. - */ - -#include - -#include -#include -#include - -#include "data_generation/llama_generator.h" - -/** - * @brief Loads brewery system prompt from disk or cache. - * - * @param prompt_file_path Preferred prompt file location. - * @return Prompt text loaded from disk. - */ -std::string LlamaGenerator::LoadBrewerySystemPrompt( - const std::filesystem::path& prompt_file_path) { - // Return cached version if already loaded - if (!brewery_system_prompt_.empty()) { - return brewery_system_prompt_; - } - - std::ifstream prompt_file(prompt_file_path); - if (!prompt_file.is_open()) { - spdlog::error( - "LlamaGenerator: Failed to open brewery system prompt file '{}'", - prompt_file_path.string()); - throw std::runtime_error( - "LlamaGenerator: missing brewery system prompt file: " + - prompt_file_path.string()); - } - - const std::string prompt((std::istreambuf_iterator(prompt_file)), - std::istreambuf_iterator()); - prompt_file.close(); - - if (prompt.empty()) { - spdlog::error("LlamaGenerator: Brewery system prompt file '{}' is empty", - prompt_file_path.string()); - throw std::runtime_error( - "LlamaGenerator: empty brewery system prompt file: " + - prompt_file_path.string()); - } - - spdlog::info( - "LlamaGenerator: Loaded brewery system prompt from '{}' ({} chars)", - prompt_file_path.string(), prompt.length()); - brewery_system_prompt_ = prompt; - return brewery_system_prompt_; -} diff --git a/tooling/pipeline/src/main.cc b/tooling/pipeline/src/main.cc index 6206f4f..241c84d 100644 --- a/tooling/pipeline/src/main.cc +++ b/tooling/pipeline/src/main.cc @@ -24,6 +24,7 @@ #include "llama_backend_state.h" #include "services/enrichment_service.h" #include "services/export_service.h" +#include "services/prompt_directory.h" #include "services/sqlite_export_service.h" #include "services/wikipedia_service.h" #include "web_client/curl_web_client.h" @@ -70,6 +71,9 @@ std::optional ParseArguments(const int argc, char** argv) { opt("log-path", prog_opts::value()->default_value("pipeline.log"), "Path for application logs"); + opt("prompt-dir", prog_opts::value()->default_value(""), + "Directory containing named prompt files (e.g. BREWERY_GENERATION.md)." + " Required when not using --mocked."); if (argc == 1) { spdlog::info("Biergarten Pipeline"); @@ -95,6 +99,7 @@ std::optional ParseArguments(const int argc, char** argv) { options.pipeline.output_path = vm["output"].as(); options.pipeline.log_path = vm["log-path"].as(); + options.pipeline.prompt_dir = vm["prompt-dir"].as(); const bool use_mocked = vm["mocked"].as(); const std::string model_path = vm["model"].as(); @@ -111,6 +116,13 @@ std::optional ParseArguments(const int argc, char** argv) { return std::nullopt; } + if (!use_mocked && options.pipeline.prompt_dir.empty()) { + spdlog::error( + "Invalid arguments: --prompt-dir is required when not using " + "--mocked"); + return std::nullopt; + } + options.generator.use_mocked = use_mocked; options.generator.model_path = model_path; @@ -172,6 +184,19 @@ int main(const int argc, char** argv) { const auto sampling = options.generator.sampling.value_or(SamplingOptions{}); + // Scenario 4: Validate the prompt directory up-front, before any DI + // wiring, so the error surfaces immediately with a clear message. + std::unique_ptr prompt_directory; + if (!options.generator.use_mocked) { + try { + prompt_directory = + std::make_unique(options.pipeline.prompt_dir); + } catch (const std::exception& dir_error) { + spdlog::error("[Startup] Invalid --prompt-dir: {}", dir_error.what()); + return 1; + } + } + const auto injector = di::make_injector( di::bind().to(), di::bind().to(options), @@ -180,8 +205,8 @@ int main(const int argc, char** argv) { di::bind().to(), di::bind().to(model_path), di::bind().to( - [options, model_path, - sampling](const auto& inj) -> std::unique_ptr { + [options, model_path, sampling, &prompt_directory]( + const auto& inj) -> std::unique_ptr { if (options.generator.use_mocked) { spdlog::info( "[Generator] Using MockGenerator (no model path provided)"); @@ -193,7 +218,13 @@ int main(const int argc, char** argv) { "top-p={}, top-k={}, n_ctx={}, seed={})", model_path, sampling.temperature, sampling.top_p, sampling.top_k, sampling.n_ctx, sampling.seed); - return inj.template create>(); + // Transfer ownership of the pre-validated PromptDirectory into + // the LlamaGenerator. The lambda captures by reference so the + // unique_ptr is moved exactly once. + return std::make_unique( + options, model_path, + inj.template create>(), + std::move(prompt_directory)); })); auto generator = diff --git a/tooling/pipeline/src/services/prompt_directory.cc b/tooling/pipeline/src/services/prompt_directory.cc new file mode 100644 index 0000000..ab908b2 --- /dev/null +++ b/tooling/pipeline/src/services/prompt_directory.cc @@ -0,0 +1,85 @@ +/** + * @file services/prompt_directory.cc + * @brief PromptDirectory implementation: validates the directory at + * construction and loads named prompt files on demand with in-process caching. + */ + +#include "services/prompt_directory.h" + +#include + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// PromptDirectory +// --------------------------------------------------------------------------- + +PromptDirectory::PromptDirectory(const std::filesystem::path& prompt_dir) + : prompt_dir_(prompt_dir) { + std::error_code ec; + + // Scenario 4: directory must exist. + if (!std::filesystem::exists(prompt_dir_, ec) || ec) { + throw std::runtime_error( + "PromptDirectory: prompt directory does not exist: " + + prompt_dir_.string()); + } + + // Scenario 4: path must be a directory, not a file. + if (!std::filesystem::is_directory(prompt_dir_, ec) || ec) { + throw std::runtime_error( + "PromptDirectory: prompt directory path is not a directory: " + + prompt_dir_.string()); + } + + // Scenario 4: directory must be readable (probe with directory_iterator). + std::filesystem::directory_iterator probe(prompt_dir_, ec); + if (ec) { + throw std::runtime_error( + "PromptDirectory: prompt directory is not readable: " + + prompt_dir_.string() + " (" + ec.message() + ")"); + } + + spdlog::info("[PromptDirectory] Resolved prompt directory: {}", + prompt_dir_.string()); +} + +std::string PromptDirectory::Load(std::string_view key) { + const std::string key_str(key); + + // Return cached content if already loaded during this run. + const auto cache_it = cache_.find(key_str); + if (cache_it != cache_.end()) { + return cache_it->second; + } + + // Scenario 3: resolve /.md and require it to exist. + const std::filesystem::path file_path = + prompt_dir_ / std::filesystem::path(key_str + ".md"); + + std::ifstream file(file_path); + if (!file.is_open()) { + throw std::runtime_error( + "PromptDirectory: prompt file not found for key '" + key_str + + "': " + file_path.string()); + } + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + + if (content.empty()) { + throw std::runtime_error("PromptDirectory: prompt file for key '" + + key_str + "' is empty: " + file_path.string()); + } + + spdlog::info("[PromptDirectory] Loaded prompt '{}' from '{}' ({} chars)", + key_str, file_path.string(), content.size()); + + cache_.emplace(key_str, content); + return content; +}