mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-17 01:47:22 +00:00
Pipeline: Add LLM and mocked user generation to the pipeline (#230)
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* @file json_handling/json_loader.cc
|
||||
* @brief Parses curated location JSON input into strongly typed Location
|
||||
* records with strict field validation and descriptive error reporting.
|
||||
*/
|
||||
|
||||
#include "services/curated_data/curated_json_data_service.h"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "services/logging/logger.h"
|
||||
|
||||
static std::string ReadRequiredString(const boost::json::object& object,
|
||||
const char* key) {
|
||||
const boost::json::value* value = object.if_contains(key);
|
||||
if (value == nullptr || !value->is_string()) {
|
||||
throw std::runtime_error(
|
||||
std::format("Missing or invalid string field: {}", key));
|
||||
}
|
||||
const std::string_view text = value->as_string();
|
||||
return std::string(text);
|
||||
}
|
||||
|
||||
static double ReadRequiredNumber(const boost::json::object& object,
|
||||
const char* key) {
|
||||
const boost::json::value* value = object.if_contains(key);
|
||||
if (value == nullptr || !value->is_number()) {
|
||||
throw std::runtime_error(
|
||||
std::format("Missing or invalid numeric field: {}", key));
|
||||
}
|
||||
return value->to_number<double>();
|
||||
}
|
||||
|
||||
static std::vector<std::string> ReadRequiredStringArray(
|
||||
const boost::json::object& object, const char* key) {
|
||||
const boost::json::value* value = object.if_contains(key);
|
||||
if (value == nullptr || !value->is_array()) {
|
||||
throw std::runtime_error(
|
||||
std::format("Missing or invalid string array field: {}", key));
|
||||
}
|
||||
|
||||
const auto& array = value->as_array();
|
||||
std::vector<std::string> items;
|
||||
items.reserve(array.size());
|
||||
for (const auto& item : array) {
|
||||
if (!item.is_string()) {
|
||||
throw std::runtime_error(
|
||||
std::format("Missing or invalid string array field: {}", key));
|
||||
}
|
||||
items.emplace_back(item.as_string());
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
boost::json::value ParseJsonFile(const std::filesystem::path& filepath,
|
||||
const char* what) {
|
||||
std::ifstream input(filepath);
|
||||
if (!input.is_open()) {
|
||||
throw std::runtime_error(
|
||||
std::format("Failed to open {} file: {}", what, filepath.string()));
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
|
||||
boost::system::error_code error;
|
||||
boost::json::value root = boost::json::parse(buffer.str(), error);
|
||||
if (error) {
|
||||
throw std::runtime_error(
|
||||
std::format("Failed to parse {} JSON: {}", what, error.message()));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the first element of a string array field, falling back to
|
||||
* `fallback_key` if `key` is missing/empty (some source entries only have a
|
||||
* "localized" form and no "romanized" form).
|
||||
*/
|
||||
std::string ReadFirstOfStringArray(const boost::json::object& object,
|
||||
const char* key, const char* fallback_key) {
|
||||
for (const char* candidate_key : {key, fallback_key}) {
|
||||
const boost::json::value* value = object.if_contains(candidate_key);
|
||||
if (value == nullptr || !value->is_array()) {
|
||||
continue;
|
||||
}
|
||||
const auto& array = value->as_array();
|
||||
if (!array.empty() && array.front().is_string()) {
|
||||
return std::string(array.front().as_string());
|
||||
}
|
||||
}
|
||||
throw std::runtime_error(
|
||||
std::format("Missing or invalid string array field: {}", key));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CuratedJsonDataService::CuratedJsonDataService(CuratedDataFilePaths filepaths)
|
||||
: filepaths_(std::move(filepaths)) {}
|
||||
|
||||
const LocationsList& CuratedJsonDataService::LoadLocations() {
|
||||
if (!cache_.locations.empty()) {
|
||||
return cache_.locations;
|
||||
}
|
||||
|
||||
const boost::json::value root =
|
||||
ParseJsonFile(filepaths_.locations_path, "locations");
|
||||
|
||||
if (!root.is_array()) {
|
||||
throw std::runtime_error(
|
||||
"Invalid locations JSON: root element must be an array");
|
||||
}
|
||||
|
||||
LocationsList locations;
|
||||
const auto& items = root.as_array();
|
||||
locations.reserve(items.size());
|
||||
|
||||
for (const auto& item : items) {
|
||||
if (!item.is_object()) {
|
||||
throw std::runtime_error(
|
||||
"Invalid locations JSON: each entry must be an object");
|
||||
}
|
||||
|
||||
const auto& object = item.as_object();
|
||||
locations.push_back(Location{
|
||||
.city = ReadRequiredString(object, "city"),
|
||||
.state_province = ReadRequiredString(object, "state_province"),
|
||||
.iso3166_2 = ReadRequiredString(object, "iso3166_2"),
|
||||
.country = ReadRequiredString(object, "country"),
|
||||
.iso3166_1 = ReadRequiredString(object, "iso3166_1"),
|
||||
.local_languages = ReadRequiredStringArray(object, "local_languages"),
|
||||
.latitude = ReadRequiredNumber(object, "latitude"),
|
||||
.longitude = ReadRequiredNumber(object, "longitude"),
|
||||
});
|
||||
}
|
||||
cache_.locations = std::move(locations);
|
||||
return cache_.locations;
|
||||
}
|
||||
|
||||
const PersonasList& CuratedJsonDataService::LoadPersonas() {
|
||||
if (!cache_.personas.empty()) {
|
||||
return cache_.personas;
|
||||
}
|
||||
|
||||
const boost::json::value root =
|
||||
ParseJsonFile(filepaths_.personas_path, "personas");
|
||||
|
||||
if (!root.is_array()) {
|
||||
throw std::runtime_error(
|
||||
"Invalid personas JSON: root element must be an array");
|
||||
}
|
||||
|
||||
PersonasList personas;
|
||||
const auto& items = root.as_array();
|
||||
personas.reserve(items.size());
|
||||
|
||||
for (const auto& item : items) {
|
||||
if (!item.is_object()) {
|
||||
throw std::runtime_error(
|
||||
"Invalid personas JSON: each entry must be an object");
|
||||
}
|
||||
|
||||
const auto& object = item.as_object();
|
||||
personas.push_back(UserPersona{
|
||||
.name = ReadRequiredString(object, "name"),
|
||||
.description = ReadRequiredString(object, "description"),
|
||||
.style_affinities = ReadRequiredStringArray(object, "style_affinities"),
|
||||
});
|
||||
}
|
||||
|
||||
cache_.personas = std::move(personas);
|
||||
return cache_.personas;
|
||||
}
|
||||
|
||||
const ForenamesByCountryMap& CuratedJsonDataService::LoadForenamesByCountry() {
|
||||
if (!cache_.forenames_by_country.empty()) {
|
||||
return cache_.forenames_by_country;
|
||||
}
|
||||
|
||||
const boost::json::value root =
|
||||
ParseJsonFile(filepaths_.forenames_path, "forenames-by-country");
|
||||
|
||||
if (!root.is_object()) {
|
||||
throw std::runtime_error(
|
||||
"Invalid forenames-by-country JSON: root element must be an object "
|
||||
"keyed by ISO 3166-1 country code");
|
||||
}
|
||||
|
||||
ForenamesByCountryMap forenames_by_country;
|
||||
for (const auto& [country_code, regions] : root.as_object()) {
|
||||
if (!regions.is_array()) {
|
||||
continue;
|
||||
}
|
||||
ForenameList entries;
|
||||
for (const auto& region : regions.as_array()) {
|
||||
if (!region.is_object()) {
|
||||
continue;
|
||||
}
|
||||
const boost::json::value* names = region.as_object().if_contains("names");
|
||||
if (names == nullptr || !names->is_array()) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& name_value : names->as_array()) {
|
||||
if (!name_value.is_object()) {
|
||||
continue;
|
||||
}
|
||||
const auto& name_object = name_value.as_object();
|
||||
entries.emplace_back(ForenameEntry{
|
||||
.name =
|
||||
ReadFirstOfStringArray(name_object, "romanized", "localized"),
|
||||
.gender = ReadRequiredString(name_object, "gender"),
|
||||
});
|
||||
}
|
||||
}
|
||||
forenames_by_country.emplace(country_code, std::move(entries));
|
||||
}
|
||||
|
||||
cache_.forenames_by_country = std::move(forenames_by_country);
|
||||
return cache_.forenames_by_country;
|
||||
}
|
||||
|
||||
const SurnamesByCountryMap& CuratedJsonDataService::LoadSurnamesByCountry() {
|
||||
if (!cache_.surnames_by_country.empty()) {
|
||||
return cache_.surnames_by_country;
|
||||
}
|
||||
|
||||
const boost::json::value root =
|
||||
ParseJsonFile(filepaths_.surnames_path, "surnames-by-country");
|
||||
|
||||
if (!root.is_object()) {
|
||||
throw std::runtime_error(
|
||||
"Invalid surnames-by-country JSON: root element must be an object "
|
||||
"keyed by ISO 3166-1 country code");
|
||||
}
|
||||
|
||||
SurnamesByCountryMap surnames_by_country;
|
||||
for (const auto& [country_code, name_entries] : root.as_object()) {
|
||||
if (!name_entries.is_array()) continue;
|
||||
|
||||
SurnameList surnames;
|
||||
for (const auto& name_value : name_entries.as_array()) {
|
||||
if (!name_value.is_object()) {
|
||||
continue;
|
||||
}
|
||||
surnames.emplace_back(ReadFirstOfStringArray(name_value.as_object(),
|
||||
"romanized", "localized"));
|
||||
}
|
||||
surnames_by_country.emplace(country_code, std::move(surnames));
|
||||
}
|
||||
|
||||
cache_.surnames_by_country = std::move(surnames_by_country);
|
||||
return cache_.surnames_by_country;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @file services/curated_data/mock_curated_data_service.cc
|
||||
* @brief Fixed in-memory location, persona, and name dataset for mock mode.
|
||||
*/
|
||||
|
||||
#include "services/curated_data/mock_curated_data_service.h"
|
||||
|
||||
MockCuratedDataService::MockCuratedDataService()
|
||||
: locations_{
|
||||
Location{.city = "Portland",
|
||||
.state_province = "Oregon",
|
||||
.iso3166_2 = "US-OR",
|
||||
.country = "United States",
|
||||
.iso3166_1 = "US",
|
||||
.local_languages = {"en"},
|
||||
.latitude = 45.5152,
|
||||
.longitude = -122.6784},
|
||||
Location{.city = "Munich",
|
||||
.state_province = "Bavaria",
|
||||
.iso3166_2 = "DE-BY",
|
||||
.country = "Germany",
|
||||
.iso3166_1 = "DE",
|
||||
.local_languages = {"de"},
|
||||
.latitude = 48.1351,
|
||||
.longitude = 11.5820},
|
||||
Location{.city = "Lyon",
|
||||
.state_province = "Auvergne-Rhone-Alpes",
|
||||
.iso3166_2 = "FR-ARA",
|
||||
.country = "France",
|
||||
.iso3166_1 = "FR",
|
||||
.local_languages = {"fr"},
|
||||
.latitude = 45.7640,
|
||||
.longitude = 4.8357},
|
||||
Location{.city = "Brussels",
|
||||
.state_province = "Brussels-Capital",
|
||||
.iso3166_2 = "BE-BRU",
|
||||
.country = "Belgium",
|
||||
.iso3166_1 = "BE",
|
||||
.local_languages = {"nl", "fr"},
|
||||
.latitude = 50.8503,
|
||||
.longitude = 4.3517},
|
||||
},
|
||||
personas_{
|
||||
UserPersona{.name = "Hophead Explorer",
|
||||
.description = "Chases hop-forward IPAs and seeks out "
|
||||
"taprooms with rotating drafts.",
|
||||
.style_affinities = {"IPA", "Pale Ale"}},
|
||||
UserPersona{.name = "Lager Traditionalist",
|
||||
.description = "Prefers clean, balanced lagers and "
|
||||
"classic pub styles.",
|
||||
.style_affinities = {"Lager", "Pilsner"}},
|
||||
UserPersona{.name = "Sour Curious",
|
||||
.description = "Seeks out wild ales, sours, and "
|
||||
"barrel-aged experiments.",
|
||||
.style_affinities = {"Sour", "Wild Ale"}},
|
||||
},
|
||||
forenames_by_country_{
|
||||
{"US",
|
||||
ForenameList{
|
||||
ForenameEntry{.name = "James", .gender = "M"},
|
||||
ForenameEntry{.name = "Mary", .gender = "F"},
|
||||
}},
|
||||
{"DE",
|
||||
ForenameList{
|
||||
ForenameEntry{.name = "Lukas", .gender = "M"},
|
||||
ForenameEntry{.name = "Anna", .gender = "F"},
|
||||
}},
|
||||
{"FR",
|
||||
ForenameList{
|
||||
ForenameEntry{.name = "Lucas", .gender = "M"},
|
||||
ForenameEntry{.name = "Camille", .gender = "F"},
|
||||
}},
|
||||
{"BE",
|
||||
ForenameList{
|
||||
ForenameEntry{.name = "Noah", .gender = "M"},
|
||||
ForenameEntry{.name = "Emma", .gender = "F"},
|
||||
}},
|
||||
},
|
||||
surnames_by_country_{
|
||||
{"US", SurnameList{"Smith", "Johnson"}},
|
||||
{"DE", SurnameList{"Muller", "Schmidt"}},
|
||||
{"FR", SurnameList{"Martin", "Bernard"}},
|
||||
{"BE", SurnameList{"Peeters", "Janssens"}},
|
||||
} {}
|
||||
|
||||
const LocationsList& MockCuratedDataService::LoadLocations() {
|
||||
return locations_;
|
||||
}
|
||||
|
||||
const PersonasList& MockCuratedDataService::LoadPersonas() { return personas_; }
|
||||
|
||||
const ForenamesByCountryMap& MockCuratedDataService::LoadForenamesByCountry() {
|
||||
return forenames_by_country_;
|
||||
}
|
||||
|
||||
const SurnamesByCountryMap& MockCuratedDataService::LoadSurnamesByCountry() {
|
||||
return surnames_by_country_;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* @file wikipedia/fetch_extract.cc
|
||||
* @brief WikipediaEnrichmentService::FetchExtract() implementation.
|
||||
*/
|
||||
|
||||
#include <boost/json.hpp>
|
||||
@@ -20,9 +21,10 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
if (const auto cache_it = this->extract_cache_.find(cache_key);
|
||||
cache_it != this->extract_cache_.end()) {
|
||||
if (logger_) {
|
||||
logger_->Log({.level = LogLevel::Debug,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("Wikipedia: Cache hit for {}!", cache_key)});
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Debug,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format("Wikipedia: Cache hit for {}!", cache_key)});
|
||||
}
|
||||
return cache_it->second;
|
||||
}
|
||||
@@ -30,7 +32,8 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
const std::string encoded = this->client_->EncodeURL(cache_key);
|
||||
const std::string url = std::format(
|
||||
"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);
|
||||
|
||||
const std::string body = this->client_->Get(url);
|
||||
@@ -45,11 +48,11 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
|
||||
if (ec) {
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("WikipediaService: JSON parse error for '{}': {}",
|
||||
std::string(query), ec.message())});
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format(
|
||||
"WikipediaService: JSON parse error for '{}': {}",
|
||||
std::string(query), ec.message())});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -58,12 +61,11 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
const json::object* obj = doc.if_object();
|
||||
if (obj == nullptr) {
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message =
|
||||
std::format("WikipediaService: Expected root object for '{}'",
|
||||
std::string(query))});
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format(
|
||||
"WikipediaService: Expected root object for '{}'",
|
||||
std::string(query))});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -76,12 +78,11 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
|
||||
if ((pages_ptr == nullptr) || !pages_ptr->is_object()) {
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message =
|
||||
std::format("WikipediaService: Missing query.pages for '{}'",
|
||||
std::string(query))});
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format(
|
||||
"WikipediaService: Missing query.pages for '{}'",
|
||||
std::string(query))});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -90,11 +91,11 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
|
||||
if (pages.empty()) {
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("WikipediaService: No pages returned for '{}'",
|
||||
std::string(query))});
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format(
|
||||
"WikipediaService: No pages returned for '{}'",
|
||||
std::string(query))});
|
||||
}
|
||||
this->extract_cache_.emplace(cache_key, "");
|
||||
return {};
|
||||
@@ -106,12 +107,11 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
|
||||
if (!page_val.is_object()) {
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message =
|
||||
std::format("WikipediaService: Unexpected page format for '{}'",
|
||||
std::string(query))});
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format(
|
||||
"WikipediaService: Unexpected page format for '{}'",
|
||||
std::string(query))});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -121,10 +121,11 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
// Handle 404/Missing status
|
||||
if (page.contains("missing")) {
|
||||
if (logger_) {
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("WikipediaService: Page '{}' does not exist",
|
||||
std::string(query))});
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format("WikipediaService: Page '{}' does not exist",
|
||||
std::string(query))});
|
||||
}
|
||||
this->extract_cache_.emplace(cache_key, "");
|
||||
return {};
|
||||
@@ -134,12 +135,11 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
|
||||
if ((extract_ptr == nullptr) || !extract_ptr->is_string()) {
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message =
|
||||
std::format("WikipediaService: No extract string found for '{}'",
|
||||
std::string(query))});
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format(
|
||||
"WikipediaService: No extract string found for '{}'",
|
||||
std::string(query))});
|
||||
}
|
||||
this->extract_cache_.emplace(cache_key, "");
|
||||
return {};
|
||||
@@ -148,10 +148,11 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) {
|
||||
// 4. Success
|
||||
std::string extract(extract_ptr->as_string());
|
||||
if (logger_) {
|
||||
logger_->Log({.level = LogLevel::Info,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("WikipediaService: Fetched {} chars for '{}'",
|
||||
extract.size(), std::string(query))});
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Info,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format("WikipediaService: Fetched {} chars for '{}'",
|
||||
extract.size(), std::string(query))});
|
||||
}
|
||||
|
||||
this->extract_cache_.insert_or_assign(cache_key, extract);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file wikipedia/get_summary.cc
|
||||
* @brief WikipediaService::GetLocationContext() implementation.
|
||||
* @brief WikipediaEnrichmentService::GetLocationContext() implementation.
|
||||
*/
|
||||
|
||||
#include <chrono>
|
||||
@@ -16,7 +16,7 @@ std::string WikipediaEnrichmentService::GetLocationContext(
|
||||
if (!this->client_) {
|
||||
if (logger_) {
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = "Wikipedia client is nullptr."});
|
||||
}
|
||||
return {};
|
||||
@@ -24,13 +24,6 @@ std::string WikipediaEnrichmentService::GetLocationContext(
|
||||
|
||||
std::string result;
|
||||
|
||||
// std::string region_query(loc.city);
|
||||
// if (!loc.country.empty()) {
|
||||
// region_query += loc.state_province,
|
||||
// region_query += ", ";
|
||||
// region_query += loc.country;
|
||||
// }
|
||||
|
||||
constexpr std::string_view brewing_query = "brewing";
|
||||
const std::string location_query =
|
||||
std::format("{}, {}", loc.city, loc.iso3166_2);
|
||||
@@ -51,9 +44,10 @@ std::string WikipediaEnrichmentService::GetLocationContext(
|
||||
append_extract(FetchExtract(beer_query));
|
||||
if (logger_) {
|
||||
logger_->Log({.level = LogLevel::Info,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("Done fetching for {}. Sleeping for 10 seconds.",
|
||||
location_query)});
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format(
|
||||
"Done fetching for {}. Sleeping for 10 seconds.",
|
||||
location_query)});
|
||||
}
|
||||
std::this_thread::sleep_for(10s);
|
||||
|
||||
@@ -61,9 +55,9 @@ std::string WikipediaEnrichmentService::GetLocationContext(
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Debug,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.phase = PipelinePhase::Enrichment,
|
||||
.message = std::format("WikipediaService lookup failed for '{}': {}",
|
||||
location_query, e.what())});
|
||||
location_query, e.what())});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file services/wikipedia/wikipedia_service.cc
|
||||
* @brief WikipediaService constructor implementation.
|
||||
* @brief WikipediaEnrichmentService constructor implementation.
|
||||
*/
|
||||
|
||||
#include "services/enrichment/wikipedia_service.h"
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace {
|
||||
switch (phase) {
|
||||
case PipelinePhase::Startup:
|
||||
return "Startup";
|
||||
case PipelinePhase::Enrichment:
|
||||
return "Enrichment";
|
||||
case PipelinePhase::UserGeneration:
|
||||
return "User Generation";
|
||||
case PipelinePhase::BreweryAndBeerGeneration:
|
||||
|
||||
@@ -44,8 +44,8 @@ PromptDirectory::PromptDirectory(const std::filesystem::path& prompt_dir,
|
||||
// Scenario 4: directory must be readable (probe with directory_iterator).
|
||||
std::filesystem::directory_iterator probe(prompt_dir_, ec);
|
||||
if (ec) {
|
||||
throw std::runtime_error(
|
||||
std::format("PromptDirectory: prompt directory is not readable: {} ({})",
|
||||
throw std::runtime_error(std::format(
|
||||
"PromptDirectory: prompt directory is not readable: {} ({})",
|
||||
prompt_dir_.string(), ec.message()));
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ std::string PromptDirectory::Load(std::string_view key) {
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error(
|
||||
std::format("PromptDirectory: prompt file not found for key '{}': {}",
|
||||
key_str, file_path.string()));
|
||||
key_str, file_path.string()));
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
@@ -84,15 +84,18 @@ std::string PromptDirectory::Load(std::string_view key) {
|
||||
file.close();
|
||||
|
||||
if (content.empty()) {
|
||||
throw std::runtime_error(std::format("PromptDirectory: prompt file for key '{}' is empty: {}",
|
||||
key_str, file_path.string()));
|
||||
throw std::runtime_error(
|
||||
std::format("PromptDirectory: prompt file for key '{}' is empty: {}",
|
||||
key_str, file_path.string()));
|
||||
}
|
||||
|
||||
if (logger_) {
|
||||
logger_->Log({.level = LogLevel::Info,
|
||||
.phase = PipelinePhase::Startup,
|
||||
.message = std::format("[PromptDirectory] Loaded prompt '{}' from '{}' ({} chars)",
|
||||
key_str, file_path.string(), content.size())});
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Info,
|
||||
.phase = PipelinePhase::Startup,
|
||||
.message = std::format(
|
||||
"[PromptDirectory] Loaded prompt '{}' from '{}' ({} chars)",
|
||||
key_str, file_path.string(), content.size())});
|
||||
}
|
||||
|
||||
cache_.emplace(key_str, content);
|
||||
|
||||
@@ -14,6 +14,7 @@ void SqliteExportService::Finalize() {
|
||||
}
|
||||
|
||||
try {
|
||||
insert_user_stmt_.reset();
|
||||
insert_brewery_stmt_.reset();
|
||||
insert_location_stmt_.reset();
|
||||
if (transaction_open_) {
|
||||
|
||||
@@ -33,7 +33,7 @@ void ResetStatement(SqliteStatementHandle& statement) {
|
||||
}
|
||||
|
||||
void Bind(const SqliteStatementHandle& statement,
|
||||
const BindParam<std::string_view>& param) {
|
||||
const BoundParam<std::string_view>& param) {
|
||||
const auto byte_count = param.value.size();
|
||||
if (byte_count > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
ThrowSqliteError(sqlite3_db_handle(statement.get()), param.action);
|
||||
@@ -60,7 +60,7 @@ void Bind(const SqliteStatementHandle& statement,
|
||||
}
|
||||
|
||||
void Bind(const SqliteStatementHandle& statement,
|
||||
const BindParam<double>& param) {
|
||||
const BoundParam<double>& param) {
|
||||
if (sqlite3_bind_double(statement.get(), param.index, param.value) !=
|
||||
SQLITE_OK) {
|
||||
ThrowSqliteError(sqlite3_db_handle(statement.get()), param.action);
|
||||
@@ -68,7 +68,7 @@ void Bind(const SqliteStatementHandle& statement,
|
||||
}
|
||||
|
||||
void Bind(const SqliteStatementHandle& statement,
|
||||
const BindParam<sqlite3_int64>& param) {
|
||||
const BoundParam<sqlite3_int64>& param) {
|
||||
if (sqlite3_bind_int64(statement.get(), param.index, param.value) !=
|
||||
SQLITE_OK) {
|
||||
ThrowSqliteError(sqlite3_db_handle(statement.get()), param.action);
|
||||
|
||||
@@ -18,9 +18,9 @@ std::filesystem::path SqliteExportService::BuildDatabasePath() const {
|
||||
std::filesystem::path candidate = output_path_ / base_filename;
|
||||
|
||||
for (int suffix = 1; std::filesystem::exists(candidate); ++suffix) {
|
||||
candidate = output_path_ /
|
||||
std::filesystem::path(std::format("biergarten_seed_{}-{}.sqlite",
|
||||
run_timestamp_utc_, suffix));
|
||||
candidate = output_path_ / std::filesystem::path(
|
||||
std::format("biergarten_seed_{}-{}.sqlite",
|
||||
run_timestamp_utc_, suffix));
|
||||
}
|
||||
|
||||
return candidate;
|
||||
@@ -33,6 +33,9 @@ void SqliteExportService::InitializeSchema() const {
|
||||
sqlite_export_service_internal::ExecSql(
|
||||
db_handle_, sqlite_export_service_internal::kCreateBreweriesTableSql,
|
||||
"Failed to create SQLite breweries table");
|
||||
sqlite_export_service_internal::ExecSql(
|
||||
db_handle_, sqlite_export_service_internal::kCreateUsersTableSql,
|
||||
"Failed to create SQLite users table");
|
||||
}
|
||||
|
||||
void SqliteExportService::PrepareStatements() {
|
||||
@@ -42,6 +45,9 @@ void SqliteExportService::PrepareStatements() {
|
||||
insert_brewery_stmt_ = sqlite_export_service_internal::PrepareStatement(
|
||||
db_handle_, sqlite_export_service_internal::kInsertBrewerySql,
|
||||
"Failed to prepare SQLite brewery insert statement");
|
||||
insert_user_stmt_ = sqlite_export_service_internal::PrepareStatement(
|
||||
db_handle_, sqlite_export_service_internal::kInsertUserSql,
|
||||
"Failed to prepare SQLite user insert statement");
|
||||
}
|
||||
|
||||
void SqliteExportService::RollbackAndCloseNoThrow() noexcept {
|
||||
@@ -54,6 +60,7 @@ void SqliteExportService::RollbackAndCloseNoThrow() noexcept {
|
||||
transaction_open_ = false;
|
||||
}
|
||||
|
||||
insert_user_stmt_.reset();
|
||||
insert_brewery_stmt_.reset();
|
||||
insert_location_stmt_.reset();
|
||||
db_handle_.reset();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @file services/sqlite/process_record.cc
|
||||
* @brief SqliteExportService::ProcessRecord() implementation.
|
||||
* @brief SqliteExportService::ProcessRecord() implementation
|
||||
* and the shared location-resolution helper.
|
||||
*/
|
||||
|
||||
#include <iomanip>
|
||||
@@ -29,111 +30,117 @@ std::string SqliteExportService::BuildLocationKey(const Location& location) {
|
||||
return key_stream.str();
|
||||
}
|
||||
|
||||
uint64_t SqliteExportService::ProcessRecord(const GeneratedBrewery& brewery) {
|
||||
sqlite3_int64 SqliteExportService::ResolveLocationId(const Location& location) {
|
||||
const std::string location_key = BuildLocationKey(location);
|
||||
const auto cached_location = location_cache_.find(location_key);
|
||||
if (cached_location != location_cache_.end()) {
|
||||
return cached_location->second;
|
||||
}
|
||||
|
||||
const std::string local_languages_json =
|
||||
sqlite_export_service_internal::SerializeVector(location.local_languages);
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationCityBindIndex,
|
||||
.value = location.city,
|
||||
.action = "Failed to bind SQLite location city"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index =
|
||||
sqlite_export_service_internal::kLocationStateProvinceBindIndex,
|
||||
.value = location.state_province,
|
||||
.action = "Failed to bind SQLite location state/province"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationIso31662BindIndex,
|
||||
.value = location.iso3166_2,
|
||||
.action = "Failed to bind SQLite location ISO 3166-2 code"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationCountryBindIndex,
|
||||
.value = location.country,
|
||||
.action = "Failed to bind SQLite location country"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationIso31661BindIndex,
|
||||
.value = location.iso3166_1,
|
||||
.action = "Failed to bind SQLite location ISO 3166-1 code"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationLanguagesBindIndex,
|
||||
.value = local_languages_json,
|
||||
.action = "Failed to bind SQLite location languages"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam{
|
||||
.index = sqlite_export_service_internal::kLocationLatitudeBindIndex,
|
||||
.value = location.latitude,
|
||||
.action = "Failed to bind SQLite location latitude"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam{
|
||||
.index = sqlite_export_service_internal::kLocationLongitudeBindIndex,
|
||||
.value = location.longitude,
|
||||
.action = "Failed to bind SQLite location longitude"});
|
||||
|
||||
sqlite_export_service_internal::StepStatement(
|
||||
db_handle_, insert_location_stmt_,
|
||||
"Failed to insert SQLite location row");
|
||||
|
||||
const sqlite3_int64 location_id =
|
||||
sqlite_export_service_internal::LastInsertRowId(db_handle_);
|
||||
location_cache_.emplace(location_key, location_id);
|
||||
sqlite_export_service_internal::ResetStatement(insert_location_stmt_);
|
||||
|
||||
return location_id;
|
||||
}
|
||||
|
||||
uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
|
||||
if (db_handle_ == nullptr || !transaction_open_) {
|
||||
throw std::runtime_error("SQLite export service is not initialized");
|
||||
}
|
||||
|
||||
const std::string location_key = BuildLocationKey(brewery.location);
|
||||
const auto cached_location = location_cache_.find(location_key);
|
||||
sqlite3_int64 location_id = 0;
|
||||
|
||||
if (cached_location != location_cache_.end()) {
|
||||
location_id = cached_location->second;
|
||||
} else {
|
||||
const std::string local_languages_json =
|
||||
sqlite_export_service_internal::SerializeVector(
|
||||
brewery.location.local_languages);
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationCityBindIndex,
|
||||
.value = brewery.location.city,
|
||||
.action = "Failed to bind SQLite location city"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
.index =
|
||||
sqlite_export_service_internal::kLocationStateProvinceBindIndex,
|
||||
.value = brewery.location.state_province,
|
||||
.action = "Failed to bind SQLite location state/province"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationIso31662BindIndex,
|
||||
.value = brewery.location.iso3166_2,
|
||||
.action = "Failed to bind SQLite location ISO 3166-2 code"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationCountryBindIndex,
|
||||
.value = brewery.location.country,
|
||||
.action = "Failed to bind SQLite location country"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationIso31661BindIndex,
|
||||
.value = brewery.location.iso3166_1,
|
||||
.action = "Failed to bind SQLite location ISO 3166-1 code"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
.index =
|
||||
sqlite_export_service_internal::kLocationLanguagesBindIndex,
|
||||
.value = local_languages_json,
|
||||
.action = "Failed to bind SQLite location languages"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BindParam{
|
||||
.index = sqlite_export_service_internal::kLocationLatitudeBindIndex,
|
||||
.value = brewery.location.latitude,
|
||||
.action = "Failed to bind SQLite location latitude"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BindParam{
|
||||
.index =
|
||||
sqlite_export_service_internal::kLocationLongitudeBindIndex,
|
||||
.value = brewery.location.longitude,
|
||||
.action = "Failed to bind SQLite location longitude"});
|
||||
|
||||
sqlite_export_service_internal::StepStatement(
|
||||
db_handle_, insert_location_stmt_,
|
||||
"Failed to insert SQLite location row");
|
||||
|
||||
location_id = sqlite_export_service_internal::LastInsertRowId(db_handle_);
|
||||
location_cache_.emplace(location_key, location_id);
|
||||
sqlite_export_service_internal::ResetStatement(insert_location_stmt_);
|
||||
}
|
||||
const sqlite3_int64 location_id = ResolveLocationId(brewery.location);
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_stmt_,
|
||||
sqlite_export_service_internal::BindParam<sqlite3_int64>{
|
||||
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
|
||||
.index = sqlite_export_service_internal::kBreweryLocationIdBindIndex,
|
||||
.value = location_id,
|
||||
.action = "Failed to bind SQLite brewery location id"});
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kBreweryEnglishNameBindIndex,
|
||||
.value = brewery.brewery.name_en,
|
||||
.action = "Failed to bind SQLite brewery English name"});
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::
|
||||
kBreweryEnglishDescriptionBindIndex,
|
||||
.value = brewery.brewery.description_en,
|
||||
.action = "Failed to bind SQLite brewery English description"});
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kBreweryLocalNameBindIndex,
|
||||
.value = brewery.brewery.name_local,
|
||||
.action = "Failed to bind SQLite brewery local name"});
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_stmt_,
|
||||
sqlite_export_service_internal::BindParam<std::string_view>{
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index =
|
||||
sqlite_export_service_internal::kBreweryLocalDescriptionBindIndex,
|
||||
.value = brewery.brewery.description_local,
|
||||
|
||||
79
tooling/pipeline/src/services/sqlite/process_user_record.cc
Normal file
79
tooling/pipeline/src/services/sqlite/process_user_record.cc
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @file services/sqlite/process_user_record.cc
|
||||
* @brief SqliteExportService::ProcessRecord(UserRecord) implementation.
|
||||
*/
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include "services/database/sqlite_export_service.h"
|
||||
#include "services/database/sqlite_export_service_helpers.h"
|
||||
|
||||
uint64_t SqliteExportService::ProcessRecord(const UserRecord& user) {
|
||||
if (db_handle_ == nullptr || !transaction_open_) {
|
||||
throw std::runtime_error("SQLite export service is not initialized");
|
||||
}
|
||||
|
||||
const sqlite3_int64 location_id = ResolveLocationId(user.location);
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
|
||||
.index = sqlite_export_service_internal::kUserLocationIdBindIndex,
|
||||
.value = location_id,
|
||||
.action = "Failed to bind SQLite user location id"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kUserFirstNameBindIndex,
|
||||
.value = user.user.first_name,
|
||||
.action = "Failed to bind SQLite user first name"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kUserLastNameBindIndex,
|
||||
.value = user.user.last_name,
|
||||
.action = "Failed to bind SQLite user last name"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kUserGenderBindIndex,
|
||||
.value = user.user.gender,
|
||||
.action = "Failed to bind SQLite user gender"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kUserUsernameBindIndex,
|
||||
.value = user.user.username,
|
||||
.action = "Failed to bind SQLite user username"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kUserBioBindIndex,
|
||||
.value = user.user.bio,
|
||||
.action = "Failed to bind SQLite user bio"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<double>{
|
||||
.index = sqlite_export_service_internal::kUserActivityWeightBindIndex,
|
||||
.value = static_cast<double>(user.user.activity_weight),
|
||||
.action = "Failed to bind SQLite user activity weight"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kUserEmailBindIndex,
|
||||
.value = user.email,
|
||||
.action = "Failed to bind SQLite user email"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kUserDateOfBirthBindIndex,
|
||||
.value = user.date_of_birth,
|
||||
.action = "Failed to bind SQLite user date of birth"});
|
||||
|
||||
sqlite_export_service_internal::StepStatement(
|
||||
db_handle_, insert_user_stmt_, "Failed to insert SQLite user row");
|
||||
|
||||
sqlite_export_service_internal::ResetStatement(insert_user_stmt_);
|
||||
|
||||
return sqlite_export_service_internal::LastInsertRowId(db_handle_);
|
||||
}
|
||||
Reference in New Issue
Block a user