mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
Add user generation feature
This commit is contained in:
@@ -91,6 +91,18 @@ class BiergartenPipelineOrchestrator {
|
||||
*/
|
||||
void GenerateBreweries(std::span<const EnrichedCity> cities);
|
||||
|
||||
/**
|
||||
* @brief Generate users grounded in sampled names and personas for
|
||||
* enriched cities.
|
||||
*
|
||||
* Loads personas.json / forenames-by-country.json / surnames-by-country.json
|
||||
* itself, mirroring how QueryCitiesWithCountries() owns its own
|
||||
* locations.json load.
|
||||
*
|
||||
* @param cities Span of enriched city data.
|
||||
*/
|
||||
void GenerateUsers(std::span<const EnrichedCity> cities);
|
||||
|
||||
/**
|
||||
* @brief Log the generated brewery results.
|
||||
*/
|
||||
@@ -98,5 +110,8 @@ class BiergartenPipelineOrchestrator {
|
||||
|
||||
/// @brief Stores generated brewery data.
|
||||
std::vector<GeneratedBrewery> generated_breweries_;
|
||||
|
||||
/// @brief Stores generated user data.
|
||||
std::vector<GeneratedUser> generated_users_;
|
||||
};
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_BIERGARTEN_DATA_GENERATOR_H_
|
||||
|
||||
@@ -28,12 +28,16 @@ class DataGenerator {
|
||||
const std::string& region_context) = 0;
|
||||
|
||||
/**
|
||||
* @brief Generates a user profile for a locale.
|
||||
* @brief Generates a user profile grounded in a sampled name and persona.
|
||||
*
|
||||
* @param locale Locale hint used by generator.
|
||||
* @param city Enriched city the user is associated with.
|
||||
* @param persona Persona archetype grounding the generated bio.
|
||||
* @param name Sampled first/last name (and gender) -- not LLM-invented.
|
||||
* @return User generation result.
|
||||
*/
|
||||
virtual UserResult GenerateUser(const std::string& locale) = 0;
|
||||
virtual UserResult GenerateUser(const EnrichedCity& city,
|
||||
const UserPersona& persona,
|
||||
const Name& name) = 0;
|
||||
};
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_DATA_GENERATOR_H_
|
||||
|
||||
@@ -65,12 +65,15 @@ class LlamaGenerator final : public DataGenerator {
|
||||
const std::string& region_context) override;
|
||||
|
||||
/**
|
||||
* @brief Generates a user profile for the provided locale.
|
||||
* @brief Generates a user profile grounded in a sampled name and persona.
|
||||
*
|
||||
* @param locale Locale hint.
|
||||
* @param city Enriched city the user is associated with.
|
||||
* @param persona Persona archetype grounding the generated bio.
|
||||
* @param name Sampled first/last name -- not LLM-invented.
|
||||
* @return Generated user profile.
|
||||
*/
|
||||
UserResult GenerateUser(const std::string& locale) override;
|
||||
UserResult GenerateUser(const EnrichedCity& city, const UserPersona& persona,
|
||||
const Name& name) override;
|
||||
|
||||
private:
|
||||
static constexpr int32_t kDefaultMaxTokens = 10000;
|
||||
|
||||
@@ -47,4 +47,18 @@ void AppendTokenPiece(const llama_vocab* vocab, llama_token token,
|
||||
std::optional<std::string> ValidateBreweryJson(const std::string& raw,
|
||||
BreweryResult& brewery_out);
|
||||
|
||||
/**
|
||||
* @brief Validates and parses user JSON output.
|
||||
*
|
||||
* Only populates `username`, `bio`, and `activity_weight` -- `first_name`
|
||||
* and `last_name` are not LLM-authored and are set separately from the
|
||||
* sampled Name.
|
||||
*
|
||||
* @param raw Raw model output.
|
||||
* @param user_out Parsed user payload.
|
||||
* @return Validation error message if invalid, or std::nullopt on success.
|
||||
*/
|
||||
std::optional<std::string> ValidateUserJson(const std::string& raw,
|
||||
UserResult& user_out);
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_LLAMA_GENERATOR_HELPERS_H_
|
||||
|
||||
@@ -28,12 +28,16 @@ class MockGenerator final : public DataGenerator {
|
||||
const std::string& region_context) override;
|
||||
|
||||
/**
|
||||
* @brief Generates deterministic user data for a locale.
|
||||
* @brief Generates deterministic user data grounded in a sampled name and
|
||||
* persona.
|
||||
*
|
||||
* @param locale Locale hint.
|
||||
* @param city Enriched city the user is associated with.
|
||||
* @param persona Persona archetype.
|
||||
* @param name Sampled first/last name. Unused for mock generation.
|
||||
* @return Generated user result.
|
||||
*/
|
||||
UserResult GenerateUser(const std::string& locale) override;
|
||||
UserResult GenerateUser(const EnrichedCity& city, const UserPersona& persona,
|
||||
const Name& name) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
@@ -44,12 +48,26 @@ class MockGenerator final : public DataGenerator {
|
||||
*/
|
||||
static size_t DeterministicHash(const Location& location);
|
||||
|
||||
/**
|
||||
* @brief Combines city, persona, and name into a stable hash value.
|
||||
*
|
||||
* @param location City and country names.
|
||||
* @param persona Persona archetype.
|
||||
* @param name Sampled first/last name.
|
||||
* @return Deterministic hash value.
|
||||
*/
|
||||
static size_t DeterministicHash(const Location& location,
|
||||
const UserPersona& persona,
|
||||
const Name& name);
|
||||
|
||||
// Hash stride constants for deterministic distribution across fixed-size
|
||||
// arrays. These coprime strides spread hash values uniformly without
|
||||
// clustering, ensuring diverse output across different hash inputs.
|
||||
static constexpr size_t kNounHashStride = 7;
|
||||
static constexpr size_t kDescriptionHashStride = 13;
|
||||
static constexpr size_t kBioHashStride = 11;
|
||||
static constexpr size_t kActivityWeightHashStride = 17;
|
||||
static constexpr size_t kActivityWeightHashRange = 1000;
|
||||
|
||||
static constexpr std::array<std::string_view, 18> kBreweryAdjectives = {
|
||||
"Craft", "Heritage", "Local", "Artisan", "Pioneer", "Golden",
|
||||
|
||||
@@ -36,11 +36,27 @@ struct BreweryResult {
|
||||
* @brief Generated user profile payload.
|
||||
*/
|
||||
struct UserResult {
|
||||
/// @brief First (given) name, copied from the sampled Name -- not
|
||||
/// LLM-invented.
|
||||
std::string first_name{};
|
||||
|
||||
/// @brief Last (family) name, copied from the sampled Name -- not
|
||||
/// LLM-invented.
|
||||
std::string last_name{};
|
||||
|
||||
/// @brief Gender associated with the sampled first name, copied from the
|
||||
/// sampled Name -- not LLM-invented.
|
||||
std::string gender{};
|
||||
|
||||
/// @brief Username handle.
|
||||
std::string username{};
|
||||
|
||||
/// @brief Short user biography.
|
||||
std::string bio{};
|
||||
|
||||
/// @brief Relative check-in/activity weight for this user, used to drive
|
||||
/// a J-curve activity profile in later pipeline phases.
|
||||
float activity_weight{};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -63,4 +79,19 @@ struct GeneratedBrewery {
|
||||
BreweryResult brewery;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper struct to store generated user data.
|
||||
*
|
||||
* `email`, `date_of_birth`, and `password` are programmatically generated by
|
||||
* the orchestrator (never LLM-authored) so a downstream auth-account seeding
|
||||
* consumer can register real accounts from the pipeline's SQLite export.
|
||||
*/
|
||||
struct GeneratedUser {
|
||||
Location location;
|
||||
UserResult user;
|
||||
std::string email{};
|
||||
std::string date_of_birth{};
|
||||
std::string password{};
|
||||
};
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_MODELS_H_
|
||||
|
||||
@@ -64,6 +64,53 @@ struct BreweryLocation {
|
||||
std::string_view country_name;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Name / Persona Models
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief A sampled first/last name pair, with the source forename's gender.
|
||||
*
|
||||
* Produced by NamesByCountry::SampleName();
|
||||
*/
|
||||
struct Name {
|
||||
/// @brief First (given) name.
|
||||
std::string first_name{};
|
||||
|
||||
/// @brief Last (family) name.
|
||||
std::string last_name{};
|
||||
|
||||
/// @brief Gender associated with the sampled forename (e.g. "M", "F"), as
|
||||
/// reported by the source dataset.
|
||||
std::string gender{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A single forename entry from the names-by-country fixture data.
|
||||
*/
|
||||
struct ForenameEntry {
|
||||
/// @brief Romanized forename.
|
||||
std::string name{};
|
||||
|
||||
/// @brief Gender associated with this forename, as reported by the source
|
||||
/// dataset (e.g. "M", "F").
|
||||
std::string gender{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A persona archetype used to ground LLM-generated user bios.
|
||||
*/
|
||||
struct UserPersona {
|
||||
/// @brief Persona display name (e.g. "Hophead Explorer").
|
||||
std::string name{};
|
||||
|
||||
/// @brief Short description of the persona's interests and voice.
|
||||
std::string description{};
|
||||
|
||||
/// @brief Beer styles this persona gravitates toward.
|
||||
std::vector<std::string> style_affinities{};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Configuration Models
|
||||
// ============================================================================
|
||||
|
||||
52
tooling/pipeline/includes/data_model/names_by_country.h
Normal file
52
tooling/pipeline/includes/data_model/names_by_country.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_NAMES_BY_COUNTRY_H_
|
||||
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_NAMES_BY_COUNTRY_H_
|
||||
|
||||
/**
|
||||
* @file data_model/names_by_country.h
|
||||
* @brief Sampling over the names-by-country fixture data.
|
||||
*/
|
||||
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "data_model/models.h"
|
||||
|
||||
/**
|
||||
* @brief Samples locale-appropriate names from curated forename/surname
|
||||
* fixture data, keyed by ISO 3166-1 country code.
|
||||
*
|
||||
* Forenames and surnames are kept in separate maps (mirroring the source
|
||||
* dataset's own shape) so per-forename gender is preserved; pairing happens
|
||||
* at sample time rather than being precomputed, so no information from the
|
||||
* source dataset is discarded up front.
|
||||
*/
|
||||
class NamesByCountry {
|
||||
public:
|
||||
NamesByCountry(
|
||||
std::unordered_map<std::string, std::vector<ForenameEntry>>
|
||||
forenames_by_country,
|
||||
std::unordered_map<std::string, std::vector<std::string>>
|
||||
surnames_by_country);
|
||||
|
||||
/**
|
||||
* @brief Samples a first/last name pair for the given country.
|
||||
*
|
||||
* @param iso3166_1 ISO 3166-1 country code to sample for.
|
||||
* @param rng Random source used for sampling.
|
||||
* @return A sampled Name, or std::nullopt if the country has no forename
|
||||
* or no surname entries.
|
||||
*/
|
||||
[[nodiscard]] std::optional<Name> SampleName(const std::string& iso3166_1,
|
||||
std::mt19937& rng) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::vector<ForenameEntry>>
|
||||
forenames_by_country_;
|
||||
std::unordered_map<std::string, std::vector<std::string>>
|
||||
surnames_by_country_;
|
||||
};
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_NAMES_BY_COUNTRY_H_
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "data_model/models.h"
|
||||
#include "data_model/names_by_country.h"
|
||||
#include "services/logging/logger.h"
|
||||
|
||||
/// @brief Loads curated world locations from a JSON file into memory.
|
||||
@@ -20,6 +21,23 @@ class JsonLoader {
|
||||
static std::vector<Location> LoadLocations(
|
||||
const std::filesystem::path& filepath,
|
||||
std::shared_ptr<ILogger> logger = nullptr);
|
||||
|
||||
/// @brief Parses a JSON array file and returns all persona records.
|
||||
static std::vector<UserPersona> LoadPersonas(
|
||||
const std::filesystem::path& filepath,
|
||||
std::shared_ptr<ILogger> logger = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Parses the names-by-country fixture pair into a sampling-capable
|
||||
* NamesByCountry.
|
||||
*
|
||||
* @param forenames_filepath Path to forenames-by-country.json.
|
||||
* @param surnames_filepath Path to surnames-by-country.json.
|
||||
*/
|
||||
static NamesByCountry LoadNamesByCountry(
|
||||
const std::filesystem::path& forenames_filepath,
|
||||
const std::filesystem::path& surnames_filepath,
|
||||
std::shared_ptr<ILogger> logger = nullptr);
|
||||
};
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_JSON_HANDLING_JSON_LOADER_H_
|
||||
|
||||
Reference in New Issue
Block a user