This commit is contained in:
Aaron Po
2026-06-21 13:13:02 -04:00
parent 4de0ea6638
commit ad97b0ea6c
27 changed files with 313 additions and 298 deletions

View File

@@ -17,7 +17,6 @@
#include "data_model/generated_models.h" #include "data_model/generated_models.h"
#include "services/database/export_service.h" #include "services/database/export_service.h"
#include "services/enrichment/enrichment_service.h" #include "services/enrichment/enrichment_service.h"
#include "services/logging/logger.h" #include "services/logging/logger.h"
/** /**
@@ -28,11 +27,12 @@
*/ */
class BiergartenPipelineOrchestrator { class BiergartenPipelineOrchestrator {
public: public:
/** /**
* @brief Constructs the orchestrator with injected pipeline dependencies. * @brief Constructs the orchestrator with injected pipeline dependencies.
* *
* @param context_service Provides regional context for locations. * @param context_service Provides regional context for locations.
* @param generator Implementation (Llama or Mock) for brewery/user generation. * @param generator Implementation (Llama or Mock) for brewery/user
* generation.
* @param exporter Database backend for persisting generated records. * @param exporter Database backend for persisting generated records.
* @param application_options CLI configuration and paths. * @param application_options CLI configuration and paths.
*/ */
@@ -52,10 +52,11 @@ class BiergartenPipelineOrchestrator {
* 3. Generate brewery data for sampled cities * 3. Generate brewery data for sampled cities
* *
* @note STRUCTURAL CONCURRENCY REQUIREMENT: * @note STRUCTURAL CONCURRENCY REQUIREMENT:
* When transitioned to a multithreaded design, this method MUST structurally * When transitioned to a multithreaded design, this method MUST
* enforce that all deployed worker threads are joined before returning (e.g. * structurally enforce that all deployed worker threads are joined before
* by using std::jthread or a structured concurrency primitive). This ensures * returning (e.g. by using std::jthread or a structured concurrency
* workers do not attempt to log to a closed channel during application teardown. * primitive). This ensures workers do not attempt to log to a closed
* channel during application teardown.
* *
* @return true if successful, false if not * @return true if successful, false if not
*/ */
@@ -95,9 +96,9 @@ class BiergartenPipelineOrchestrator {
* @brief Generate users grounded in sampled names and personas for * @brief Generate users grounded in sampled names and personas for
* enriched cities. * enriched cities.
* *
* Loads personas.json / forenames-by-country.json / surnames-by-country.json * Loads personas.json / forenames-by-country.json /
* itself, mirroring how QueryCitiesWithCountries() owns its own * surnames-by-country.json itself, mirroring how QueryCitiesWithCountries()
* locations.json load. * owns its own locations.json load.
* *
* @param cities Span of enriched city data. * @param cities Span of enriched city data.
*/ */

View File

@@ -9,10 +9,12 @@
/** /**
* @file bounded_channel.h * @file bounded_channel.h
* @brief Thread-safe, bounded multi-producer/multi-consumer synchronous channel. * @brief Thread-safe, bounded multi-producer/multi-consumer synchronous
* channel.
* *
* Intent: Enables asynchronous inter-thread communication with backpressure. * Intent: Enables asynchronous inter-thread communication with backpressure.
* Models a synchronous channel where producers/consumers block on capacity limits. * Models a synchronous channel where producers/consumers block on capacity
* limits.
*/ */
/** /**

View File

@@ -57,8 +57,7 @@ class MockGenerator final : public DataGenerator {
* @return Deterministic hash value. * @return Deterministic hash value.
*/ */
static size_t DeterministicHash(const Location& location, static size_t DeterministicHash(const Location& location,
const UserPersona& persona, const UserPersona& persona, const Name& name);
const Name& name);
// Hash stride constants for deterministic distribution across fixed-size // Hash stride constants for deterministic distribution across fixed-size
// arrays. These coprime strides spread hash values uniformly without // arrays. These coprime strides spread hash values uniformly without
@@ -142,7 +141,8 @@ class MockGenerator final : public DataGenerator {
"Visits breweries for the stories, stays for the flagship pours.", "Visits breweries for the stories, stays for the flagship pours.",
"Craft beer fan mapping tasting notes and favorite brew routes.", "Craft beer fan mapping tasting notes and favorite brew routes.",
"Always ready to trade recommendations for underrated local breweries.", "Always ready to trade recommendations for underrated local breweries.",
"Keeping a running list of must-try collab releases and tap takeovers."}; "Keeping a running list of must-try collab releases and tap "
"takeovers."};
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_MOCK_GENERATOR_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_MOCK_GENERATOR_H_

View File

@@ -3,8 +3,8 @@
/** /**
* @file data_model/generated_models.h * @file data_model/generated_models.h
* @brief Generated output models from the pipeline: brewery/user results, enriched data, * @brief Generated output models from the pipeline: brewery/user results,
* and complete generation results. * enriched data, and complete generation results.
*/ */
#include <string> #include <string>

View File

@@ -148,8 +148,6 @@ struct GeneratorOptions {
/// @brief Use mocked generator instead of actual LLM inference. /// @brief Use mocked generator instead of actual LLM inference.
bool use_mocked = false; bool use_mocked = false;
/// @brief Specific sampling parameters for this generator. /// @brief Specific sampling parameters for this generator.
/// If nullopt, the application should use global defaults. /// If nullopt, the application should use global defaults.
std::optional<SamplingOptions> sampling; std::optional<SamplingOptions> sampling;
@@ -186,7 +184,7 @@ struct ApplicationOptions {
// Function Declarations // Function Declarations
// ============================================================================ // ============================================================================
std::optional<ApplicationOptions> ParseArguments(const int argc, char** argv, std::optional<ApplicationOptions> ParseArguments(
std::shared_ptr<ILogger> logger = nullptr); const int argc, char** argv, std::shared_ptr<ILogger> logger = nullptr);
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_MODELS_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_MODELS_H_

View File

@@ -25,8 +25,7 @@
*/ */
class NamesByCountry { class NamesByCountry {
public: public:
NamesByCountry( NamesByCountry(std::unordered_map<std::string, std::vector<ForenameEntry>>
std::unordered_map<std::string, std::vector<ForenameEntry>>
forenames_by_country, forenames_by_country,
std::unordered_map<std::string, std::vector<std::string>> std::unordered_map<std::string, std::vector<std::string>>
surnames_by_country); surnames_by_country);

View File

@@ -11,8 +11,8 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include "data_model/models.h"
#include "../datetime/date_time_provider.h" #include "../datetime/date_time_provider.h"
#include "data_model/models.h"
#include "export_service.h" #include "export_service.h"
#include "sqlite_export_service_helpers.h" #include "sqlite_export_service_helpers.h"

View File

@@ -46,7 +46,8 @@ enum class PipelinePhase {
/** /**
* @struct LogDTO * @struct LogDTO
* @brief User-provided subset of log fields. Used to capture call-site info transparently. * @brief User-provided subset of log fields. Used to capture call-site info
* transparently.
*/ */
struct LogDTO { struct LogDTO {
LogLevel level; LogLevel level;
@@ -74,7 +75,6 @@ struct LogEntry {
/// @brief Thread responsible for emitting the log. /// @brief Thread responsible for emitting the log.
std::thread::id thread_id{}; std::thread::id thread_id{};
/// @brief Severity level of this entry. /// @brief Severity level of this entry.
LogLevel level; LogLevel level;
@@ -83,7 +83,6 @@ struct LogEntry {
/// @brief Log message text. /// @brief Log message text.
std::string message; std::string message;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOG_ENTRY_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOG_ENTRY_H_

View File

@@ -39,7 +39,8 @@ class ILogger {
*/ */
void Log(LogDTO payload, void Log(LogDTO payload,
std::source_location origin = std::source_location::current(), std::source_location origin = std::source_location::current(),
std::chrono::system_clock::time_point timestamp = std::chrono::system_clock::now(), std::chrono::system_clock::time_point timestamp =
std::chrono::system_clock::now(),
std::thread::id thread_id = std::this_thread::get_id()) { std::thread::id thread_id = std::this_thread::get_id()) {
LogEntry entry; LogEntry entry;
entry.timestamp = timestamp; entry.timestamp = timestamp;

View File

@@ -1,19 +1,18 @@
/** /**
* @file web_client/http_web_client.h * @file web_client/http_web_client.h
* @brief cpp-httplib implementation of the WebClient interface. * @brief cpp-httplib implementation of the WebClient interface.
*/ */
#ifndef BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_HTTP_WEB_CLIENT_H_ #ifndef BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_HTTP_WEB_CLIENT_H_
#define BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_HTTP_WEB_CLIENT_H_ #define BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_HTTP_WEB_CLIENT_H_
#include "web_client/web_client.h"
#include "services/logging/logger.h"
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
#include "services/logging/logger.h"
#include "web_client/web_client.h"
/** /**
* @brief WebClient implementation backed by cpp-httplib. * @brief WebClient implementation backed by cpp-httplib.
* *
@@ -26,7 +25,7 @@
* bound to a single origin at construction time. * bound to a single origin at construction time.
*/ */
class HttpWebClient final : public WebClient { class HttpWebClient final : public WebClient {
public: public:
explicit HttpWebClient(std::shared_ptr<ILogger> logger) explicit HttpWebClient(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {} : logger_(std::move(logger)) {}
~HttpWebClient() override = default; ~HttpWebClient() override = default;
@@ -34,7 +33,8 @@ public:
/** /**
* @brief Executes a blocking HTTP/HTTPS GET request against a full URL. * @brief Executes a blocking HTTP/HTTPS GET request against a full URL.
* *
* @param url Fully-qualified URL, e.g. "https://en.wikipedia.org/api/rest_v1/page/summary/Berlin" * @param url Fully-qualified URL, e.g.
* "https://en.wikipedia.org/api/rest_v1/page/summary/Berlin"
* @return Response body on HTTP 2xx; throws std::runtime_error otherwise. * @return Response body on HTTP 2xx; throws std::runtime_error otherwise.
*/ */
std::string Get(const std::string& url) override; std::string Get(const std::string& url) override;
@@ -52,5 +52,4 @@ public:
std::shared_ptr<ILogger> logger_; std::shared_ptr<ILogger> logger_;
}; };
#endif #endif

View File

@@ -51,7 +51,8 @@ std::optional<ApplicationOptions> ParseArguments(
prog_opts::value<std::string>()->default_value("pipeline.log"), prog_opts::value<std::string>()->default_value("pipeline.log"),
"Path for application logs"); "Path for application logs");
opt("prompt-dir", prog_opts::value<std::string>()->default_value(""), opt("prompt-dir", prog_opts::value<std::string>()->default_value(""),
"Directory containing named prompt files (e.g. BREWERY_GENERATION.md)." "Directory containing named prompt files (e.g. "
"BREWERY_GENERATION.md)."
" Required when not using --mocked."); " Required when not using --mocked.");
opt("location-count", prog_opts::value<uint32_t>()->default_value(10)); opt("location-count", prog_opts::value<uint32_t>()->default_value(10));
}; };
@@ -106,10 +107,12 @@ std::optional<ApplicationOptions> ParseArguments(
const std::string model_path = var_map["model"].as<std::string>(); const std::string model_path = var_map["model"].as<std::string>();
const int n_gpu_layers = var_map["n-gpu-layers"].as<int>(); const int n_gpu_layers = var_map["n-gpu-layers"].as<int>();
// Enforce mutual exclusivity before any further configuration is applied. // Enforce mutual exclusivity before any further configuration is
// applied.
if (use_mocked && !model_path.empty()) { if (use_mocked && !model_path.empty()) {
const std::string msg = const std::string msg =
"Invalid arguments: --mocked and --model are mutually exclusive"; "Invalid arguments: --mocked and --model are mutually "
"exclusive";
if (logger) { if (logger) {
logger->Log(LogDTO{.level = LogLevel::Error, logger->Log(LogDTO{.level = LogLevel::Error,
.phase = PipelinePhase::Startup, .phase = PipelinePhase::Startup,
@@ -122,7 +125,8 @@ std::optional<ApplicationOptions> ParseArguments(
if (!use_mocked && model_path.empty()) { if (!use_mocked && model_path.empty()) {
const std::string msg = const std::string msg =
"Invalid arguments: either --mocked or --model must be specified"; "Invalid arguments: either --mocked or --model must be "
"specified";
if (logger) { if (logger) {
logger->Log(LogDTO{.level = LogLevel::Error, logger->Log(LogDTO{.level = LogLevel::Error,
.phase = PipelinePhase::Startup, .phase = PipelinePhase::Startup,
@@ -137,7 +141,8 @@ std::optional<ApplicationOptions> ParseArguments(
// generator has no use for it and should not require it to be present. // generator has no use for it and should not require it to be present.
if (!use_mocked && options.pipeline.prompt_dir.empty()) { if (!use_mocked && options.pipeline.prompt_dir.empty()) {
const std::string msg = const std::string msg =
"Invalid arguments: --prompt-dir is required when not using --mocked"; "Invalid arguments: --prompt-dir is required when not using "
"--mocked";
if (logger) { if (logger) {
logger->Log({.level = LogLevel::Error, logger->Log({.level = LogLevel::Error,
.phase = PipelinePhase::Startup, .phase = PipelinePhase::Startup,

View File

@@ -12,7 +12,7 @@ BiergartenPipelineOrchestrator::BiergartenPipelineOrchestrator(
std::unique_ptr<IEnrichmentService> context_service, std::unique_ptr<IEnrichmentService> context_service,
std::unique_ptr<DataGenerator> generator, std::unique_ptr<DataGenerator> generator,
std::unique_ptr<IExportService> exporter, std::unique_ptr<IExportService> exporter,
const ApplicationOptions &app_options) const ApplicationOptions& app_options)
: logger_(std::move(logger)), : logger_(std::move(logger)),
context_service_(std::move(context_service)), context_service_(std::move(context_service)),
generator_(std::move(generator)), generator_(std::move(generator)),

View File

@@ -36,22 +36,26 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
logger_->Log( logger_->Log(
{.level = LogLevel::Warn, {.level = LogLevel::Warn,
.phase = PipelinePhase::BreweryAndBeerGeneration, .phase = PipelinePhase::BreweryAndBeerGeneration,
.message = .message = std::format(
std::format("[Pipeline] Generated brewery for '{}' ({}) but SQLite export failed: {}", "[Pipeline] Generated brewery for '{}' ({}) "
"but SQLite export failed: {}",
location.city, location.country, export_exception.what())}); location.city, location.country, export_exception.what())});
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
++skipped_count; ++skipped_count;
logger_->Log({.level = LogLevel::Warn, logger_->Log(
{.level = LogLevel::Warn,
.phase = PipelinePhase::BreweryAndBeerGeneration, .phase = PipelinePhase::BreweryAndBeerGeneration,
.message = std::format("[Pipeline] Skipping city '{}' ({}): brewery generation failed: {}", .message = std::format("[Pipeline] Skipping city '{}' ({}): brewery "
"generation failed: {}",
location.city, location.country, e.what())}); location.city, location.country, e.what())});
} }
} }
if (skipped_count > 0) { if (skipped_count > 0) {
logger_->Log({.level = LogLevel::Warn, logger_->Log(
{.level = LogLevel::Warn,
.phase = PipelinePhase::BreweryAndBeerGeneration, .phase = PipelinePhase::BreweryAndBeerGeneration,
.message = std::format( .message = std::format(
"[Pipeline] Skipped {} city/cities due to generation errors", "[Pipeline] Skipped {} city/cities due to generation errors",
@@ -59,10 +63,11 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
} }
if (export_failed_count > 0) { if (export_failed_count > 0) {
logger_->Log({.level = LogLevel::Warn, logger_->Log(
{.level = LogLevel::Warn,
.phase = PipelinePhase::Teardown, .phase = PipelinePhase::Teardown,
.message = std::format( .message = std::format("[Pipeline] Failed to export {} generated "
"[Pipeline] Failed to export {} generated brewery/breweries to SQLite", "brewery/breweries to SQLite",
export_failed_count)}); export_failed_count)});
} }
} }

View File

@@ -24,8 +24,8 @@ std::string Sanitize(std::string_view value) {
out.reserve(value.size()); out.reserve(value.size());
for (const char character : value) { for (const char character : value) {
if (std::isalnum(static_cast<unsigned char>(character)) != 0) { if (std::isalnum(static_cast<unsigned char>(character)) != 0) {
out.push_back( out.push_back(static_cast<char>(
static_cast<char>(std::tolower(static_cast<unsigned char>(character)))); std::tolower(static_cast<unsigned char>(character))));
} }
} }
return out; return out;
@@ -62,8 +62,7 @@ std::string GenerateDateOfBirth(std::mt19937& rng) {
sys_days{birth_year_anchor} - days{day_offset_dist(rng)}; sys_days{birth_year_anchor} - days{day_offset_dist(rng)};
const year_month_day birth_ymd{birth_date}; const year_month_day birth_ymd{birth_date};
return std::format("{:04}-{:02}-{:02}", return std::format("{:04}-{:02}-{:02}", static_cast<int>(birth_ymd.year()),
static_cast<int>(birth_ymd.year()),
static_cast<unsigned>(birth_ymd.month()), static_cast<unsigned>(birth_ymd.month()),
static_cast<unsigned>(birth_ymd.day())); static_cast<unsigned>(birth_ymd.day()));
} }
@@ -71,7 +70,8 @@ std::string GenerateDateOfBirth(std::mt19937& rng) {
std::string GenerateRandomPassword(std::mt19937& rng) { std::string GenerateRandomPassword(std::mt19937& rng) {
constexpr size_t kPasswordLength = 32; constexpr size_t kPasswordLength = 32;
constexpr std::string_view kCharset = constexpr std::string_view kCharset =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&"
"*";
std::uniform_int_distribution<size_t> char_dist(0, kCharset.size() - 1); std::uniform_int_distribution<size_t> char_dist(0, kCharset.size() - 1);
@@ -115,11 +115,11 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
if (!sampled_name.has_value()) { if (!sampled_name.has_value()) {
++skipped_count; ++skipped_count;
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::UserGeneration, .phase = PipelinePhase::UserGeneration,
.message = std::format( .message = std::format(
"[Pipeline] Skipping city '{}' ({}): no names available for " "[Pipeline] Skipping city '{}' ({}): no names "
"available for "
"country '{}'", "country '{}'",
city.location.city, city.location.country, city.location.city, city.location.country,
city.location.iso3166_1)}); city.location.iso3166_1)});
@@ -147,21 +147,21 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
} catch (const std::exception& export_exception) { } catch (const std::exception& export_exception) {
++export_failed_count; ++export_failed_count;
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::UserGeneration, .phase = PipelinePhase::UserGeneration,
.message = std::format( .message = std::format(
"[Pipeline] Generated user for '{}' ({}) but SQLite export failed: {}", "[Pipeline] Generated user for '{}' ({}) but "
"SQLite export failed: {}",
city.location.city, city.location.country, city.location.city, city.location.country,
export_exception.what())}); export_exception.what())});
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
++skipped_count; ++skipped_count;
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::UserGeneration, .phase = PipelinePhase::UserGeneration,
.message = std::format( .message = std::format(
"[Pipeline] Skipping city '{}' ({}): user generation failed: {}", "[Pipeline] Skipping city '{}' ({}): "
"user generation failed: {}",
city.location.city, city.location.country, e.what())}); city.location.city, city.location.country, e.what())});
} }
} }
@@ -176,11 +176,10 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
} }
if (export_failed_count > 0) { if (export_failed_count > 0) {
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::Teardown, .phase = PipelinePhase::Teardown,
.message = std::format( .message = std::format("[Pipeline] Failed to export {} "
"[Pipeline] Failed to export {} generated user/users to SQLite", "generated user/users to SQLite",
export_failed_count)}); export_failed_count)});
} }
} }

View File

@@ -38,7 +38,6 @@ void BiergartenPipelineOrchestrator::LogResults() const {
boost::json::array user_output; boost::json::array user_output;
for (const auto& generated_user : generated_users_) { for (const auto& generated_user : generated_users_) {
user_output.push_back(boost::json::object{ user_output.push_back(boost::json::object{
{"first_name", generated_user.user.first_name}, {"first_name", generated_user.user.first_name},

View File

@@ -28,20 +28,21 @@ bool BiergartenPipelineOrchestrator::Run() {
.region_context = std::move(region_context)}); .region_context = std::move(region_context)});
} catch (const std::exception& exception) { } catch (const std::exception& exception) {
++skipped_count; ++skipped_count;
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = std::format( .message = std::format(
"[Pipeline] Skipping city '{}' ({}): context lookup failed: {}", "[Pipeline] Skipping city '{}' ({}): context "
"lookup failed: {}",
city.city, city.country, exception.what())}); city.city, city.country, exception.what())});
} }
} }
if (skipped_count > 0) { if (skipped_count > 0) {
logger_->Log({.level = LogLevel::Warn, logger_->Log(
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = std::format( .message = std::format("[Pipeline] Skipped {} city/cities due "
"[Pipeline] Skipped {} city/cities due to context lookup errors", "to context lookup errors",
skipped_count)}); skipped_count)});
} }
@@ -51,11 +52,10 @@ bool BiergartenPipelineOrchestrator::Run() {
this->LogResults(); this->LogResults();
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
logger_->Log( logger_->Log({.level = LogLevel::Error,
{.level = LogLevel::Error,
.phase = PipelinePhase::Teardown, .phase = PipelinePhase::Teardown,
.message = .message = std::format(
std::format("Pipeline execution failed with error: {}", e.what())}); "Pipeline execution failed with error: {}", e.what())});
return false; return false;
} }
} }

View File

@@ -83,8 +83,8 @@ BreweryResult LlamaGenerator::GenerateBrewery(
/** /**
* RETRY LOOP with validation and error correction * RETRY LOOP with validation and error correction
* Attempts to generate valid brewery data up to 3 times, with feedback-based * Attempts to generate valid brewery data up to 3 times, with
* refinement * feedback-based refinement
*/ */
constexpr int max_attempts = 3; constexpr int max_attempts = 3;
std::string raw; std::string raw;
@@ -119,7 +119,8 @@ BreweryResult LlamaGenerator::GenerateBrewery(
logger_->Log( logger_->Log(
{.level = LogLevel::Info, {.level = LogLevel::Info,
.phase = PipelinePhase::BreweryAndBeerGeneration, .phase = PipelinePhase::BreweryAndBeerGeneration,
.message = std::format("LlamaGenerator: successfully generated brewery data on attempt {}", .message = std::format("LlamaGenerator: successfully generated "
"brewery data on attempt {}",
attempt + 1)}); attempt + 1)});
} }
@@ -133,32 +134,37 @@ BreweryResult LlamaGenerator::GenerateBrewery(
logger_->Log( logger_->Log(
{.level = LogLevel::Warn, {.level = LogLevel::Warn,
.phase = PipelinePhase::BreweryAndBeerGeneration, .phase = PipelinePhase::BreweryAndBeerGeneration,
.message = .message = std::format(
std::format("LlamaGenerator: malformed brewery JSON (attempt {}): {}", "LlamaGenerator: malformed brewery JSON (attempt {}): {}",
attempt + 1, *validation_error)}); attempt + 1, *validation_error)});
} }
// Update prompt with error details to guide LLM toward correct output. // Update prompt with error details to guide LLM toward correct output.
user_prompt = std::format( user_prompt = std::format(
"Your previous response was invalid. Error: {}\nReturn the thought " "Your previous response was invalid. Error: {}\nReturn the thought "
"process before the JSON if needed, then return ONLY valid JSON with " "process before the JSON if needed, then return ONLY valid JSON "
"exactly these keys, in this exact order: {{\"name_en\": \"<English " "with "
"exactly these keys, in this exact order: {{\"name_en\": "
"\"<English "
"brewery name>\", \"description_en\": \"<English single-paragraph " "brewery name>\", \"description_en\": \"<English single-paragraph "
"description>\", \"name_local\": \"<local-language brewery name>\", " "description>\", \"name_local\": \"<local-language brewery "
"name>\", "
"\"description_local\": \"<local-language single-paragraph " "\"description_local\": \"<local-language single-paragraph "
"description>\"}}.\nDo not include markdown, comments, extra keys, or " "description>\"}}.\nDo not include markdown, comments, extra keys, "
"literal placeholder values.\n\nKeep the JSON strings concise enough " "or "
"literal placeholder values.\n\nKeep the JSON strings concise "
"enough "
"to fit within the token budget.\n\n{}", "to fit within the token budget.\n\n{}",
*validation_error, retry_location); *validation_error, retry_location);
} }
// All retry attempts exhausted: log failure and throw exception // All retry attempts exhausted: log failure and throw exception
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Error,
{.level = LogLevel::Error,
.phase = PipelinePhase::BreweryAndBeerGeneration, .phase = PipelinePhase::BreweryAndBeerGeneration,
.message = std::format( .message = std::format(
"LlamaGenerator: malformed brewery response after {} attempts: {}", "LlamaGenerator: malformed brewery "
"response after {} attempts: {}",
max_attempts, last_error.empty() ? raw : last_error)}); max_attempts, last_error.empty() ? raw : last_error)});
} }
throw std::runtime_error("LlamaGenerator: malformed brewery response"); throw std::runtime_error("LlamaGenerator: malformed brewery response");

View File

@@ -41,17 +41,17 @@ UserResult LlamaGenerator::GenerateUser(const EnrichedCity& city,
const std::string system_prompt = prompt_directory_->Load("USER_GENERATION"); const std::string system_prompt = prompt_directory_->Load("USER_GENERATION");
std::string user_prompt = std::format( std::string user_prompt = std::format(
"## NAME:\n{} {}\n\n## GENDER:\n{}\n\n## CITY:\n{}\n\n## COUNTRY:\n{}\n\n" "## NAME:\n{} {}\n\n## GENDER:\n{}\n\n## CITY:\n{}\n\n## "
"COUNTRY:\n{}\n\n"
"## PERSONA:\n{}\n\n## PERSONA DESCRIPTION:\n{}\n\n## STYLE " "## PERSONA:\n{}\n\n## PERSONA DESCRIPTION:\n{}\n\n## STYLE "
"AFFINITIES:\n{}", "AFFINITIES:\n{}",
name.first_name, name.last_name, name.gender, city.location.city, name.first_name, name.last_name, name.gender, city.location.city,
city.location.country, persona.name, persona.description, city.location.country, persona.name, persona.description,
style_affinities); style_affinities);
const std::string retry_context = const std::string retry_context = std::format(
std::format("Name: {} {}\nCity: {}, {}\nPersona: {}", name.first_name, "Name: {} {}\nCity: {}, {}\nPersona: {}", name.first_name, name.last_name,
name.last_name, city.location.city, city.location.country, city.location.city, city.location.country, persona.name);
persona.name);
constexpr int max_attempts = 3; constexpr int max_attempts = 3;
std::string raw; std::string raw;
@@ -59,8 +59,7 @@ UserResult LlamaGenerator::GenerateUser(const EnrichedCity& city,
int max_tokens = kUserInitialMaxTokens; int max_tokens = kUserInitialMaxTokens;
for (int attempt = 0; attempt < max_attempts; ++attempt) { for (int attempt = 0; attempt < max_attempts; ++attempt) {
raw = this->Infer(system_prompt, user_prompt, max_tokens, raw = this->Infer(system_prompt, user_prompt, max_tokens, kUserJsonGrammar);
kUserJsonGrammar);
if (logger_) { if (logger_) {
logger_->Log( logger_->Log(
{.level = LogLevel::Debug, {.level = LogLevel::Debug,
@@ -78,7 +77,8 @@ UserResult LlamaGenerator::GenerateUser(const EnrichedCity& city,
logger_->Log( logger_->Log(
{.level = LogLevel::Info, {.level = LogLevel::Info,
.phase = PipelinePhase::UserGeneration, .phase = PipelinePhase::UserGeneration,
.message = std::format("LlamaGenerator: successfully generated user data on attempt {}", .message = std::format("LlamaGenerator: successfully "
"generated user data on attempt {}",
attempt + 1)}); attempt + 1)});
} }
@@ -90,31 +90,33 @@ UserResult LlamaGenerator::GenerateUser(const EnrichedCity& city,
last_error = *validation_error; last_error = *validation_error;
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::UserGeneration, .phase = PipelinePhase::UserGeneration,
.message = .message = std::format(
std::format("LlamaGenerator: malformed user JSON (attempt {}): {}", "LlamaGenerator: malformed user JSON (attempt {}): {}",
attempt + 1, *validation_error)}); attempt + 1, *validation_error)});
} }
user_prompt = std::format( user_prompt = std::format(
"Your previous response was invalid. Error: {}\nReturn the thought " "Your previous response was invalid. Error: {}\nReturn the thought "
"process before the JSON if needed, then return ONLY valid JSON with " "process before the JSON if needed, then return ONLY valid JSON "
"exactly these keys, in this exact order: {{\"username\": \"<handle " "with "
"exactly these keys, in this exact order: {{\"username\": "
"\"<handle "
"derived from the given name>\", \"bio\": \"<first-person bio " "derived from the given name>\", \"bio\": \"<first-person bio "
"grounded in the persona>\", \"activity_weight\": <number between 0 " "grounded in the persona>\", \"activity_weight\": <number between "
"0 "
"and 1>}}.\nDo not include markdown, comments, extra keys, or " "and 1>}}.\nDo not include markdown, comments, extra keys, or "
"literal placeholder values.\n\n{}", "literal placeholder values.\n\n{}",
*validation_error, retry_context); *validation_error, retry_context);
} }
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Error,
{.level = LogLevel::Error,
.phase = PipelinePhase::UserGeneration, .phase = PipelinePhase::UserGeneration,
.message = std::format( .message = std::format(
"LlamaGenerator: malformed user response after {} attempts: {}", "LlamaGenerator: malformed user response "
"after {} attempts: {}",
max_attempts, last_error.empty() ? raw : last_error)}); max_attempts, last_error.empty() ? raw : last_error)});
} }
throw std::runtime_error("LlamaGenerator: malformed user response"); throw std::runtime_error("LlamaGenerator: malformed user response");

View File

@@ -29,8 +29,7 @@ std::optional<Name> NamesByCountry::SampleName(const std::string& iso3166_1,
const std::vector<ForenameEntry>& forenames = forenames_it->second; const std::vector<ForenameEntry>& forenames = forenames_it->second;
const std::vector<std::string>& surnames = surnames_it->second; const std::vector<std::string>& surnames = surnames_it->second;
std::uniform_int_distribution<size_t> forename_dist(0, std::uniform_int_distribution<size_t> forename_dist(0, forenames.size() - 1);
forenames.size() - 1);
std::uniform_int_distribution<size_t> surname_dist(0, surnames.size() - 1); std::uniform_int_distribution<size_t> surname_dist(0, surnames.size() - 1);
const ForenameEntry& forename = forenames[forename_dist(rng)]; const ForenameEntry& forename = forenames[forename_dist(rng)];

View File

@@ -6,12 +6,10 @@
#include "json_handling/json_loader.h" #include "json_handling/json_loader.h"
#include <format>
#include "services/logging/logger.h"
#include <iostream>
#include <boost/json.hpp> #include <boost/json.hpp>
#include <format>
#include <fstream> #include <fstream>
#include <iostream>
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
@@ -19,6 +17,8 @@
#include <unordered_map> #include <unordered_map>
#include <utility> #include <utility>
#include "services/logging/logger.h"
static std::string ReadRequiredString(const boost::json::object& object, static std::string ReadRequiredString(const boost::json::object& object,
const char* key) { const char* key) {
const boost::json::value* value = object.if_contains(key); const boost::json::value* value = object.if_contains(key);
@@ -67,8 +67,8 @@ boost::json::value ParseJsonFile(const std::filesystem::path& filepath,
const char* what) { const char* what) {
std::ifstream input(filepath); std::ifstream input(filepath);
if (!input.is_open()) { if (!input.is_open()) {
throw std::runtime_error(std::format("Failed to open {} file: {}", what, throw std::runtime_error(
filepath.string())); std::format("Failed to open {} file: {}", what, filepath.string()));
} }
std::stringstream buffer; std::stringstream buffer;
@@ -87,8 +87,7 @@ boost::json::value ParseJsonFile(const std::filesystem::path& filepath,
/// `fallback_key` if `key` is missing/empty (some source entries only have a /// `fallback_key` if `key` is missing/empty (some source entries only have a
/// "localized" form and no "romanized" form). /// "localized" form and no "romanized" form).
std::string ReadFirstOfStringArray(const boost::json::object& object, std::string ReadFirstOfStringArray(const boost::json::object& object,
const char* key, const char* key, const char* fallback_key) {
const char* fallback_key) {
for (const char* candidate_key : {key, fallback_key}) { for (const char* candidate_key : {key, fallback_key}) {
const boost::json::value* value = object.if_contains(candidate_key); const boost::json::value* value = object.if_contains(candidate_key);
if (value == nullptr || !value->is_array()) { if (value == nullptr || !value->is_array()) {
@@ -163,8 +162,7 @@ std::vector<UserPersona> JsonLoader::LoadPersonas(
personas.push_back(UserPersona{ personas.push_back(UserPersona{
.name = ReadRequiredString(object, "name"), .name = ReadRequiredString(object, "name"),
.description = ReadRequiredString(object, "description"), .description = ReadRequiredString(object, "description"),
.style_affinities = .style_affinities = ReadRequiredStringArray(object, "style_affinities"),
ReadRequiredStringArray(object, "style_affinities"),
}); });
} }
@@ -207,8 +205,8 @@ NamesByCountry JsonLoader::LoadNamesByCountry(
} }
const auto& name_object = name_value.as_object(); const auto& name_object = name_value.as_object();
entries.push_back(ForenameEntry{ entries.push_back(ForenameEntry{
.name = ReadFirstOfStringArray(name_object, "romanized", .name =
"localized"), ReadFirstOfStringArray(name_object, "romanized", "localized"),
.gender = ReadRequiredString(name_object, "gender"), .gender = ReadRequiredString(name_object, "gender"),
}); });
} }
@@ -216,8 +214,7 @@ NamesByCountry JsonLoader::LoadNamesByCountry(
forenames_by_country.emplace(country_code, std::move(entries)); forenames_by_country.emplace(country_code, std::move(entries));
} }
std::unordered_map<std::string, std::vector<std::string>> std::unordered_map<std::string, std::vector<std::string>> surnames_by_country;
surnames_by_country;
for (const auto& [country_code, name_entries] : surnames_root.as_object()) { for (const auto& [country_code, name_entries] : surnames_root.as_object()) {
if (!name_entries.is_array()) { if (!name_entries.is_array()) {
continue; continue;

View File

@@ -176,7 +176,8 @@ int main(const int argc, char** argv) {
{.level = LogLevel::Info, {.level = LogLevel::Info,
.phase = PipelinePhase::Startup, .phase = PipelinePhase::Startup,
.message = std::format( .message = std::format(
"Generator: LlamaGenerator | model={} | temp={:.2f} " "Generator: LlamaGenerator | model={} | "
"temp={:.2f} "
"top_p={:.2f} top_k={} n_ctx={} seed={}", "top_p={:.2f} top_k={} n_ctx={} seed={}",
model_path, sampling.temperature, sampling.top_p, model_path, sampling.temperature, sampling.top_p,
sampling.top_k, sampling.n_ctx, sampling.seed)}); sampling.top_k, sampling.n_ctx, sampling.seed)});

View File

@@ -20,7 +20,8 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
if (const auto cache_it = this->extract_cache_.find(cache_key); if (const auto cache_it = this->extract_cache_.find(cache_key);
cache_it != this->extract_cache_.end()) { cache_it != this->extract_cache_.end()) {
if (logger_) { if (logger_) {
logger_->Log({.level = LogLevel::Debug, logger_->Log(
{.level = LogLevel::Debug,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = std::format("Wikipedia: Cache hit for {}!", cache_key)}); .message = std::format("Wikipedia: Cache hit for {}!", cache_key)});
} }
@@ -30,7 +31,8 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
const std::string encoded = this->client_->EncodeURL(cache_key); const std::string encoded = this->client_->EncodeURL(cache_key);
const std::string url = std::format( const std::string url = std::format(
"https://en.wikipedia.org/w/" "https://en.wikipedia.org/w/"
"api.php?action=query&titles={}&prop=extracts&explaintext=1&format=json", "api.php?action=query&titles={}&prop=extracts&explaintext=1&format="
"json",
encoded); encoded);
const std::string body = this->client_->Get(url); const std::string body = this->client_->Get(url);
@@ -45,10 +47,10 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
if (ec) { if (ec) {
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = std::format("WikipediaService: JSON parse error for '{}': {}", .message = std::format(
"WikipediaService: JSON parse error for '{}': {}",
std::string(query), ec.message())}); std::string(query), ec.message())});
} }
return {}; return {};
@@ -58,11 +60,10 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
const json::object* obj = doc.if_object(); const json::object* obj = doc.if_object();
if (obj == nullptr) { if (obj == nullptr) {
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = .message = std::format(
std::format("WikipediaService: Expected root object for '{}'", "WikipediaService: Expected root object for '{}'",
std::string(query))}); std::string(query))});
} }
return {}; return {};
@@ -76,11 +77,10 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
if ((pages_ptr == nullptr) || !pages_ptr->is_object()) { if ((pages_ptr == nullptr) || !pages_ptr->is_object()) {
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = .message = std::format(
std::format("WikipediaService: Missing query.pages for '{}'", "WikipediaService: Missing query.pages for '{}'",
std::string(query))}); std::string(query))});
} }
return {}; return {};
@@ -90,10 +90,10 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
if (pages.empty()) { if (pages.empty()) {
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = std::format("WikipediaService: No pages returned for '{}'", .message = std::format(
"WikipediaService: No pages returned for '{}'",
std::string(query))}); std::string(query))});
} }
this->extract_cache_.emplace(cache_key, ""); this->extract_cache_.emplace(cache_key, "");
@@ -106,11 +106,10 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
if (!page_val.is_object()) { if (!page_val.is_object()) {
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = .message = std::format(
std::format("WikipediaService: Unexpected page format for '{}'", "WikipediaService: Unexpected page format for '{}'",
std::string(query))}); std::string(query))});
} }
return {}; return {};
@@ -121,7 +120,8 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
// Handle 404/Missing status // Handle 404/Missing status
if (page.contains("missing")) { if (page.contains("missing")) {
if (logger_) { if (logger_) {
logger_->Log({.level = LogLevel::Warn, logger_->Log(
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = std::format("WikipediaService: Page '{}' does not exist", .message = std::format("WikipediaService: Page '{}' does not exist",
std::string(query))}); std::string(query))});
@@ -134,11 +134,10 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
if ((extract_ptr == nullptr) || !extract_ptr->is_string()) { if ((extract_ptr == nullptr) || !extract_ptr->is_string()) {
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Warn,
{.level = LogLevel::Warn,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = .message = std::format(
std::format("WikipediaService: No extract string found for '{}'", "WikipediaService: No extract string found for '{}'",
std::string(query))}); std::string(query))});
} }
this->extract_cache_.emplace(cache_key, ""); this->extract_cache_.emplace(cache_key, "");
@@ -148,7 +147,8 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
// 4. Success // 4. Success
std::string extract(extract_ptr->as_string()); std::string extract(extract_ptr->as_string());
if (logger_) { if (logger_) {
logger_->Log({.level = LogLevel::Info, logger_->Log(
{.level = LogLevel::Info,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = std::format("WikipediaService: Fetched {} chars for '{}'", .message = std::format("WikipediaService: Fetched {} chars for '{}'",
extract.size(), std::string(query))}); extract.size(), std::string(query))});

View File

@@ -45,7 +45,8 @@ std::string WikipediaEnrichmentService::GetLocationContext(
if (logger_) { if (logger_) {
logger_->Log({.level = LogLevel::Info, logger_->Log({.level = LogLevel::Info,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = std::format("Done fetching for {}. Sleeping for 10 seconds.", .message = std::format(
"Done fetching for {}. Sleeping for 10 seconds.",
location_query)}); location_query)});
} }
std::this_thread::sleep_for(10s); std::this_thread::sleep_for(10s);

View File

@@ -44,8 +44,8 @@ PromptDirectory::PromptDirectory(const std::filesystem::path& prompt_dir,
// Scenario 4: directory must be readable (probe with directory_iterator). // Scenario 4: directory must be readable (probe with directory_iterator).
std::filesystem::directory_iterator probe(prompt_dir_, ec); std::filesystem::directory_iterator probe(prompt_dir_, ec);
if (ec) { if (ec) {
throw std::runtime_error( throw std::runtime_error(std::format(
std::format("PromptDirectory: prompt directory is not readable: {} ({})", "PromptDirectory: prompt directory is not readable: {} ({})",
prompt_dir_.string(), ec.message())); prompt_dir_.string(), ec.message()));
} }
@@ -84,14 +84,17 @@ std::string PromptDirectory::Load(std::string_view key) {
file.close(); file.close();
if (content.empty()) { if (content.empty()) {
throw std::runtime_error(std::format("PromptDirectory: prompt file for key '{}' is empty: {}", throw std::runtime_error(
std::format("PromptDirectory: prompt file for key '{}' is empty: {}",
key_str, file_path.string())); key_str, file_path.string()));
} }
if (logger_) { if (logger_) {
logger_->Log({.level = LogLevel::Info, logger_->Log(
{.level = LogLevel::Info,
.phase = PipelinePhase::Startup, .phase = PipelinePhase::Startup,
.message = std::format("[PromptDirectory] Loaded prompt '{}' from '{}' ({} chars)", .message = std::format(
"[PromptDirectory] Loaded prompt '{}' from '{}' ({} chars)",
key_str, file_path.string(), content.size())}); key_str, file_path.string(), content.size())});
} }

View File

@@ -18,8 +18,8 @@ std::filesystem::path SqliteExportService::BuildDatabasePath() const {
std::filesystem::path candidate = output_path_ / base_filename; std::filesystem::path candidate = output_path_ / base_filename;
for (int suffix = 1; std::filesystem::exists(candidate); ++suffix) { for (int suffix = 1; std::filesystem::exists(candidate); ++suffix) {
candidate = output_path_ / candidate = output_path_ / std::filesystem::path(
std::filesystem::path(std::format("biergarten_seed_{}-{}.sqlite", std::format("biergarten_seed_{}-{}.sqlite",
run_timestamp_utc_, suffix)); run_timestamp_utc_, suffix));
} }

View File

@@ -38,8 +38,7 @@ sqlite3_int64 SqliteExportService::ResolveLocationId(const Location& location) {
} }
const std::string local_languages_json = const std::string local_languages_json =
sqlite_export_service_internal::SerializeVector( sqlite_export_service_internal::SerializeVector(location.local_languages);
location.local_languages);
sqlite_export_service_internal::Bind( sqlite_export_service_internal::Bind(
insert_location_stmt_, insert_location_stmt_,
@@ -92,7 +91,8 @@ sqlite3_int64 SqliteExportService::ResolveLocationId(const Location& location) {
.action = "Failed to bind SQLite location longitude"}); .action = "Failed to bind SQLite location longitude"});
sqlite_export_service_internal::StepStatement( sqlite_export_service_internal::StepStatement(
db_handle_, insert_location_stmt_, "Failed to insert SQLite location row"); db_handle_, insert_location_stmt_,
"Failed to insert SQLite location row");
const sqlite3_int64 location_id = const sqlite3_int64 location_id =
sqlite_export_service_internal::LastInsertRowId(db_handle_); sqlite_export_service_internal::LastInsertRowId(db_handle_);

View File

@@ -48,18 +48,17 @@ std::string HttpWebClient::Get(const std::string& url) {
const httplib::Result result = client.Get(path); const httplib::Result result = client.Get(path);
if (!result) { if (!result) {
throw std::runtime_error(std::format( throw std::runtime_error(
"[HttpWebClient] Request failed for URL: {} — {}", url, std::format("[HttpWebClient] Request failed for URL: {} — {}", url,
httplib::to_string(result.error()))); httplib::to_string(result.error())));
} }
if (result->status < kSuccessMin || result->status >= kSuccessMax) { if (result->status < kSuccessMin || result->status >= kSuccessMax) {
if (logger_) { if (logger_) {
logger_->Log( logger_->Log({.level = LogLevel::Error,
{.level = LogLevel::Error,
.phase = PipelinePhase::Enrichment, .phase = PipelinePhase::Enrichment,
.message = .message = std::format(
std::format("[HttpWebClient] Request failed for URL: {}", url)}); "[HttpWebClient] Request failed for URL: {}", url)});
} }
throw std::runtime_error(std::format("[HttpWebClient] HTTP {} for URL: {}", throw std::runtime_error(std::format("[HttpWebClient] HTTP {} for URL: {}",
result->status, url)); result->status, url));