From e738fe484aa738b0b45a39bcdac3c7bc87961982 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Sun, 23 Aug 2026 15:49:28 -0400 Subject: [PATCH] Update generate pipeline to add OpenAI api as an additional backend (for comparison testing), add brewery coordinates (#244) * add brewery coordinates * Add openai mode * Add coordinates to schema * Fix C++ review findings in pipeline: k-prefixed enum values, include ordering, populated validation error messages, conventional const notation, and endl replaced with newline --- tooling/pipeline/CMakeLists.txt | 13 + .../pipeline/includes/biergarten_pipeline.h | 3 + .../generation_json_validation.h | 52 ++++ .../data_generation/llama_generator_helpers.h | 43 +--- .../data_generation/openai_generator.h | 104 ++++++++ .../data_generation/openai_json_schemas.h | 46 ++++ .../includes/data_model/generated_models.h | 2 + tooling/pipeline/includes/data_model/models.h | 35 ++- .../database/sqlite_statement_helpers.h | 18 +- .../includes/web_client/http_web_client.h | 17 ++ .../pipeline/includes/web_client/web_client.h | 16 ++ .../application_options/parse_arguments.cc | 64 +++-- .../generate_breweries.cc | 59 ++++- .../log_results.cc | 42 ++-- .../query_cities_with_countries.cc | 13 +- .../generation_json_validation.cc | 236 ++++++++++++++++++ .../data_generation/llama/generate_brewery.cc | 2 +- .../data_generation/llama/generate_user.cc | 2 +- .../src/data_generation/llama/helpers.cc | 221 +--------------- .../openai/call_chat_completions_api.cc | 182 ++++++++++++++ .../openai/generate_brewery.cc | 132 ++++++++++ .../data_generation/openai/generate_user.cc | 114 +++++++++ .../openai/openai_generator.cc | 20 ++ tooling/pipeline/src/main.cc | 58 +++-- .../curated_data/curated_json_data_service.cc | 16 +- .../src/services/sqlite/process_record.cc | 26 ++ .../src/web_client/http_web_client.cc | 39 +++ .../Database.Seed/Database.Seed.csproj | 2 +- .../DatabaseHelpers/ConnectionStrings.cs | 4 +- .../PipelineData/BreweryAddress.cs | 4 + .../SeedData/open-ai-test.sqlite | Bin 0 -> 94208 bytes .../SeedData/open-ai-test2.sqlite | Bin 0 -> 286720 bytes .../Sqlite/PipelineSeedDataReader.cs | 6 +- .../Domain.Entities/Entities/BreweryPost.cs | 1 + web/generate-env.sh | 78 ++++++ 35 files changed, 1327 insertions(+), 343 deletions(-) create mode 100644 tooling/pipeline/includes/data_generation/generation_json_validation.h create mode 100644 tooling/pipeline/includes/data_generation/openai_generator.h create mode 100644 tooling/pipeline/includes/data_generation/openai_json_schemas.h create mode 100644 tooling/pipeline/src/data_generation/generation_json_validation.cc create mode 100644 tooling/pipeline/src/data_generation/openai/call_chat_completions_api.cc create mode 100644 tooling/pipeline/src/data_generation/openai/generate_brewery.cc create mode 100644 tooling/pipeline/src/data_generation/openai/generate_user.cc create mode 100644 tooling/pipeline/src/data_generation/openai/openai_generator.cc create mode 100644 web/backend/Database/Database.Seed/SeedData/open-ai-test.sqlite create mode 100644 web/backend/Database/Database.Seed/SeedData/open-ai-test2.sqlite create mode 100755 web/generate-env.sh diff --git a/tooling/pipeline/CMakeLists.txt b/tooling/pipeline/CMakeLists.txt index 220334bf..33a813fd 100644 --- a/tooling/pipeline/CMakeLists.txt +++ b/tooling/pipeline/CMakeLists.txt @@ -192,6 +192,19 @@ target_sources(${PROJECT_NAME} PRIVATE src/data_generation/mock/deterministic_hash.cc ) +# --- data_generation: shared JSON validation (always built — no llama.cpp dependency) --- +target_sources(${PROJECT_NAME} PRIVATE + src/data_generation/generation_json_validation.cc +) + +# --- data_generation: openai (always built — no llama.cpp dependency) --- +target_sources(${PROJECT_NAME} PRIVATE + src/data_generation/openai/openai_generator.cc + src/data_generation/openai/call_chat_completions_api.cc + src/data_generation/openai/generate_brewery.cc + src/data_generation/openai/generate_user.cc +) + # --- data_generation: llama (skipped for mock-only builds) --- if(NOT BIERGARTEN_MOCK_ONLY) target_sources(${PROJECT_NAME} PRIVATE diff --git a/tooling/pipeline/includes/biergarten_pipeline.h b/tooling/pipeline/includes/biergarten_pipeline.h index 6a66d5ed..74cd93f7 100644 --- a/tooling/pipeline/includes/biergarten_pipeline.h +++ b/tooling/pipeline/includes/biergarten_pipeline.h @@ -15,12 +15,15 @@ // --- data_generation --- #include "data_generation/data_generator.h" +#include "data_generation/generation_json_validation.h" #include "data_generation/json_grammars.h" #ifndef BIERGARTEN_MOCK_ONLY #include "data_generation/llama_generator.h" #include "data_generation/llama_generator_helpers.h" #endif #include "data_generation/mock_generator.h" +#include "data_generation/openai_generator.h" +#include "data_generation/openai_json_schemas.h" #include "data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.h" #include "data_generation/prompt_formatting/prompt_formatter.h" diff --git a/tooling/pipeline/includes/data_generation/generation_json_validation.h b/tooling/pipeline/includes/data_generation/generation_json_validation.h new file mode 100644 index 00000000..5f7f099a --- /dev/null +++ b/tooling/pipeline/includes/data_generation/generation_json_validation.h @@ -0,0 +1,52 @@ +#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_GENERATION_JSON_VALIDATION_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_GENERATION_JSON_VALIDATION_H_ + +/** + * @file data_generation/generation_json_validation.h + * @brief Generator-agnostic helpers for preparing prompt context and + * validating structured JSON output. Shared across all DataGenerator + * implementations (Llama, OpenAI, ...). + */ + +#include +#include +#include +#include + +#include "data_model/generated_models.h" + +/** + * @brief Normalizes and truncates regional context. + * + * @param region_context Input regional context text. + * @param max_chars Maximum output length. + * @return Processed region context. + */ +std::string PrepareRegionContext(std::string_view region_context, + size_t max_chars = 2000); + +/** + * @brief Validates and parses brewery JSON output. + * + * @param raw Raw model output. + * @param brewery_out Parsed brewery payload. + * @return Validation error message if invalid, or std::nullopt on success. + */ +std::optional 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 ValidateUserJson(const std::string& raw, + UserResult& user_out); + +#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_GENERATION_JSON_VALIDATION_H_ diff --git a/tooling/pipeline/includes/data_generation/llama_generator_helpers.h b/tooling/pipeline/includes/data_generation/llama_generator_helpers.h index 6363cb40..a9c1be6e 100644 --- a/tooling/pipeline/includes/data_generation/llama_generator_helpers.h +++ b/tooling/pipeline/includes/data_generation/llama_generator_helpers.h @@ -3,30 +3,17 @@ /** * @file data_generation/llama_generator_helpers.h - * @brief Shared helper APIs used by LlamaGenerator translation units. + * @brief Llama.cpp-specific helper APIs used by LlamaGenerator translation + * units. Generator-agnostic helpers (prompt-context prep, JSON validation) + * live in data_generation/generation_json_validation.h instead. */ -#include #include -#include #include -#include - -#include "data_model/generated_models.h" struct llama_vocab; using llama_token = int32_t; -/** - * @brief Normalizes and truncates regional context. - * - * @param region_context Input regional context text. - * @param max_chars Maximum output length. - * @return Processed region context. - */ -std::string PrepareRegionContext(std::string_view region_context, - size_t max_chars = 2000); - /** * @brief Decodes a sampled token and appends it to output text. * @@ -37,28 +24,4 @@ std::string PrepareRegionContext(std::string_view region_context, void AppendTokenPiece(const llama_vocab* vocab, llama_token token, std::string& output); -/** - * @brief Validates and parses brewery JSON output. - * - * @param raw Raw model output. - * @param brewery_out Parsed brewery payload. - * @return Validation error message if invalid, or std::nullopt on success. - */ -std::optional 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 ValidateUserJson(const std::string& raw, - UserResult& user_out); - #endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_LLAMA_GENERATOR_HELPERS_H_ diff --git a/tooling/pipeline/includes/data_generation/openai_generator.h b/tooling/pipeline/includes/data_generation/openai_generator.h new file mode 100644 index 00000000..27f759cc --- /dev/null +++ b/tooling/pipeline/includes/data_generation/openai_generator.h @@ -0,0 +1,104 @@ +#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_OPENAI_GENERATOR_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_OPENAI_GENERATOR_H_ + +/** + * @file data_generation/openai_generator.h + * @brief OpenAI Chat Completions API-backed implementation of DataGenerator. + */ + +#include +#include +#include + +#include "data_generation/data_generator.h" +#include "data_model/models.h" +#include "services/logging/logger.h" +#include "services/prompting/prompt_directory.h" +#include "web_client/web_client.h" + +/** + * @brief Data generator implementation backed by the OpenAI Chat + * Completions API. + * + * Unlike LlamaGenerator, this generator has no local model state and no + * chat-template formatting step -- Chat Completions takes system/user + * messages directly, and Structured Outputs (response_format: json_schema, + * strict: true) guarantee schema-valid JSON, replacing llama.cpp's GBNF + * grammar constraint. + */ +class OpenAIGenerator final : public DataGenerator { + public: + /** + * @brief Constructs a generator that calls the OpenAI Chat Completions + * API. + * + * @param api_key OpenAI API key (from the OPENAI_API_KEY environment + * variable -- never accepted as a CLI argument). + * @param model OpenAI model ID, e.g. "gpt-4o-mini". + * @param logger Sink for pipeline diagnostics. + * @param prompt_directory Directory service for loading named prompt + * files (the same BREWERY_GENERATION/USER_GENERATION prompts Llama + * uses -- they're backend-agnostic). + * @param web_client HTTP client used to reach the Chat Completions API. + */ + OpenAIGenerator(std::string api_key, std::string model, + std::shared_ptr logger, + std::unique_ptr prompt_directory, + std::unique_ptr web_client); + + ~OpenAIGenerator() override = default; + + OpenAIGenerator(const OpenAIGenerator&) = delete; + OpenAIGenerator& operator=(const OpenAIGenerator&) = delete; + OpenAIGenerator(OpenAIGenerator&&) = delete; + OpenAIGenerator& operator=(OpenAIGenerator&&) = delete; + + /** + * @brief Generates brewery data for a specific location. + * + * @param enriched_city Enriched city the brewery is associated with. + * @return Generated brewery result. + */ + BreweryResult GenerateBrewery(const EnrichedCity& enriched_city) override; + + /** + * @brief Generates a user profile grounded in a sampled name and persona. + * + * @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 EnrichedCity& city, const UserPersona& persona, + const Name& name) override; + + private: + /** + * @brief Sends a single Chat Completions request with a Structured + * Outputs schema and returns the raw JSON text of the response. + * + * @param system_prompt System role message content. + * @param user_prompt User role message content. + * @param json_schema Raw JSON Schema text constraining the response + * shape (see data_generation/openai_json_schemas.h). + * @param schema_name Identifier for the schema, required by the + * response_format.json_schema wrapper. + * @param max_tokens Maximum completion tokens to generate. + * @return Raw JSON text produced by the model. + * @throws std::runtime_error on a request/HTTP failure, a safety-policy + * refusal, or a response with no message content. + */ + std::string CallChatCompletionsApi(const std::string& system_prompt, + const std::string& user_prompt, + std::string_view json_schema, + std::string_view schema_name, + int max_tokens); + + std::string api_key_; + std::string model_; + std::shared_ptr logger_; + std::unique_ptr prompt_directory_; + std::unique_ptr web_client_; +}; + +#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_OPENAI_GENERATOR_H_ diff --git a/tooling/pipeline/includes/data_generation/openai_json_schemas.h b/tooling/pipeline/includes/data_generation/openai_json_schemas.h new file mode 100644 index 00000000..8cc8f31c --- /dev/null +++ b/tooling/pipeline/includes/data_generation/openai_json_schemas.h @@ -0,0 +1,46 @@ +#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_OPENAI_JSON_SCHEMAS_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_OPENAI_JSON_SCHEMAS_H_ + +/** + * @file data_generation/openai_json_schemas.h + * @brief JSON Schemas constraining structured output from OpenAIGenerator + * Chat Completions requests, via response_format.json_schema (Structured + * Outputs, strict mode). This is OpenAI's equivalent of the GBNF grammars + * in json_grammars.h -- the API guarantees the response message content is + * a single JSON string that validates against the schema, so there's no + * grammar-level "thought-block" prefix to account for. + * + * OpenAI's strict mode requires "additionalProperties": false on every + * object in the schema, which these already have. + */ + +#include + +inline constexpr std::string_view kBreweryJsonSchema = R"json_brewery( +{ + "type": "object", + "properties": { + "name_en": {"type": "string"}, + "description_en": {"type": "string"}, + "name_local": {"type": "string"}, + "description_local": {"type": "string"} + }, + "required": ["name_en", "description_en", "name_local", "description_local"], + "additionalProperties": false +} +)json_brewery"; + +inline constexpr std::string_view kUserJsonSchema = R"json_user( +{ + "type": "object", + "properties": { + "username": {"type": "string"}, + "bio": {"type": "string"}, + "activity_weight": {"type": "number"} + }, + "required": ["username", "bio", "activity_weight"], + "additionalProperties": false +} +)json_user"; + +#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_OPENAI_JSON_SCHEMAS_H_ diff --git a/tooling/pipeline/includes/data_model/generated_models.h b/tooling/pipeline/includes/data_model/generated_models.h index c2e62377..89b2cc9d 100644 --- a/tooling/pipeline/includes/data_model/generated_models.h +++ b/tooling/pipeline/includes/data_model/generated_models.h @@ -94,6 +94,8 @@ struct EnrichedCity { struct BreweryAddress { City city; std::string postal_code{}; + double longitude{}; + double latitude{}; }; /** diff --git a/tooling/pipeline/includes/data_model/models.h b/tooling/pipeline/includes/data_model/models.h index 3705e134..2666c3c1 100644 --- a/tooling/pipeline/includes/data_model/models.h +++ b/tooling/pipeline/includes/data_model/models.h @@ -66,6 +66,9 @@ struct City { */ std::string iso3166_1{}; + double longitude{}; + double latitude{}; + /** * @brief Local language codes in priority order. */ @@ -184,25 +187,47 @@ struct SamplingOptions { int n_gpu_layers = 0; }; +/** + * @brief Selects which DataGenerator backend the pipeline uses. + */ +enum class GeneratorMode { + kMock, + kLlama, + kOpenAI, +}; + /** * @brief Configuration for the LLM generator component. */ struct GeneratorOptions { /** - * @brief Path to the LLM model file (gguf format). + * @brief Path to the LLM model file (gguf format). Only meaningful for + * GeneratorMode::kLlama. */ std::filesystem::path model_path; /** - * @brief Use mocked generator instead of actual LLM inference. + * @brief Which generator backend to use. */ - bool use_mocked = false; + GeneratorMode mode = GeneratorMode::kMock; /** - * @brief Specific sampling parameters for this generator. - * If nullopt, the application should use global defaults. + * @brief Specific sampling parameters for this generator. Only + * meaningful for GeneratorMode::kLlama; if nullopt, LlamaGenerator uses + * its own defaults. */ std::optional sampling; + + /** + * @brief OpenAI API key, read from the OPENAI_API_KEY environment + * variable. Only populated for GeneratorMode::kOpenAI. + */ + std::string openai_api_key; + + /** + * @brief OpenAI model ID used by OpenAIGenerator. + */ + std::string openai_model = "gpt-4o-mini"; }; /** diff --git a/tooling/pipeline/includes/services/database/sqlite_statement_helpers.h b/tooling/pipeline/includes/services/database/sqlite_statement_helpers.h index fde53882..8b7a2507 100644 --- a/tooling/pipeline/includes/services/database/sqlite_statement_helpers.h +++ b/tooling/pipeline/includes/services/database/sqlite_statement_helpers.h @@ -26,6 +26,8 @@ CREATE TABLE IF NOT EXISTS cities ( iso3166_2 TEXT NOT NULL, country TEXT NOT NULL, iso3166_1 TEXT NOT NULL, + longitude REAL NOT NULL, + latitude REAL NOT NULL, local_languages_json TEXT NOT NULL, postal_code_country_format_regex TEXT NOT NULL, postal_code_city_regex_json TEXT NOT NULL, @@ -53,6 +55,8 @@ CREATE TABLE IF NOT EXISTS brewery_addresses ( brewery_id INTEGER NOT NULL, city_id INTEGER NOT NULL, postal_code TEXT NOT NULL, + longitude REAL NOT NULL, + latitude REAL NOT NULL, FOREIGN KEY(brewery_id) REFERENCES breweries(id) ON DELETE CASCADE, FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE ); @@ -105,10 +109,12 @@ INSERT INTO cities ( iso3166_2, country, iso3166_1, + longitude, + latitude, local_languages_json, postal_code_country_format_regex, postal_code_city_regex_json -) VALUES (?, ?, ?, ?, ?, ?, ?, ?); +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); )sql"; inline constexpr std::string_view kInsertBrewerySql = R"sql( @@ -124,8 +130,10 @@ inline constexpr std::string_view kInsertBreweryAddressSql = R"sql( INSERT INTO brewery_addresses ( brewery_id, city_id, - postal_code -) VALUES (?, ?, ?); + postal_code, + longitude, + latitude +) VALUES (?, ?, ?, ?, ?); )sql"; inline constexpr std::string_view kInsertUserSql = R"sql( @@ -157,6 +165,8 @@ enum CityBindIndex { kCityIso31662BindIndex, kCityCountryBindIndex, kCityIso31661BindIndex, + kCityLongitudeBindIndex, + kCityLatitudeBindIndex, kCityLanguagesBindIndex, kCityPostalCodeCountryFormatRegexBindIndex, kCityPostalCodeCityRegexJsonBindIndex, @@ -173,6 +183,8 @@ enum BreweryAddressBindIndex { kBreweryAddressBreweryIdBindIndex = 1, kBreweryAddressCityIdBindIndex, kBreweryAddressPostalCodeBindIndex, + kBreweryAddressLongitudeBindIndex, + kBreweryAddressLatitudeBindIndex, }; enum UserBindIndex { diff --git a/tooling/pipeline/includes/web_client/http_web_client.h b/tooling/pipeline/includes/web_client/http_web_client.h index 2db9a277..78b46c64 100644 --- a/tooling/pipeline/includes/web_client/http_web_client.h +++ b/tooling/pipeline/includes/web_client/http_web_client.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "services/logging/logger.h" #include "web_client/web_client.h" @@ -39,6 +40,22 @@ class HttpWebClient final : public WebClient { */ std::string Get(const std::string& url) override; + /** + * @brief Executes a blocking HTTP/HTTPS POST request against a full URL. + * + * Sends @p body with a "Content-Type: application/json" header, plus any + * additional headers supplied by the caller (e.g. authentication). + * + * @param url Fully-qualified URL. + * @param body Request body, sent as-is. + * @param headers Additional request headers. + * @return Response body on HTTP 2xx; throws std::runtime_error otherwise. + */ + std::string Post( + const std::string& url, const std::string& body, + const std::vector>& headers) + override; + /** * @brief Percent-encodes a single URI component (query parameter value or * path segment). Delegates to httplib::encode_uri_component(). diff --git a/tooling/pipeline/includes/web_client/web_client.h b/tooling/pipeline/includes/web_client/web_client.h index 7d290088..46a587d6 100644 --- a/tooling/pipeline/includes/web_client/web_client.h +++ b/tooling/pipeline/includes/web_client/web_client.h @@ -7,6 +7,8 @@ */ #include +#include +#include /** * @brief Abstract web client interface. @@ -23,6 +25,20 @@ class WebClient { */ virtual std::string Get(const std::string& url) = 0; + /** + * @brief Executes an HTTP POST request with a JSON body. + * + * @param url Request URL. + * @param body Request body (sent as-is; callers are responsible for + * providing valid JSON when the target API expects it). + * @param headers Additional request headers (e.g. authentication), + * sent alongside a fixed "Content-Type: application/json" header. + * @return Response body. + */ + virtual std::string Post( + const std::string& url, const std::string& body, + const std::vector>& headers) = 0; + /** * @brief URL-encodes a string value. * diff --git a/tooling/pipeline/src/application_options/parse_arguments.cc b/tooling/pipeline/src/application_options/parse_arguments.cc index 2e7fff1e..1822584b 100644 --- a/tooling/pipeline/src/application_options/parse_arguments.cc +++ b/tooling/pipeline/src/application_options/parse_arguments.cc @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -37,13 +38,20 @@ std::optional ParseArguments( "Number of layers to offload to GPU"); }; - // --mocked and --model are mutually exclusive; validation is enforced below - // rather than at registration to produce a clear diagnostic message. + // --mocked, --model, and --openai are mutually exclusive; validation is + // enforced below rather than at registration to produce a clear + // diagnostic message. auto add_generator_options = [&]() -> void { opt("mocked", prog_opts::bool_switch(), "Use mocked generator for brewery/user data"); opt("model,m", prog_opts::value()->default_value(""), "Path to LLM model (gguf)"); + opt("openai", prog_opts::bool_switch(), + "Use the OpenAI API (Chat Completions) for brewery/user data. " + "Requires the OPENAI_API_KEY environment variable."); + opt("openai-model", + prog_opts::value()->default_value("gpt-4o-mini"), + "OpenAI model ID used when --openai is set"); }; auto add_pipeline_options = [&]() -> void { @@ -109,14 +117,19 @@ std::optional ParseArguments( const bool use_mocked = var_map["mocked"].as(); const std::string model_path = var_map["model"].as(); + const bool use_openai = var_map["openai"].as(); const int n_gpu_layers = var_map["n-gpu-layers"].as(); // Enforce mutual exclusivity before any further configuration is // applied. - if (use_mocked && !model_path.empty()) { + const int selected_generator_count = + (use_mocked ? 1 : 0) + (!model_path.empty() ? 1 : 0) + + (use_openai ? 1 : 0); + + if (selected_generator_count > 1) { const std::string msg = - "Invalid arguments: --mocked and --model are mutually " - "exclusive"; + "Invalid arguments: --mocked, --model, and --openai are " + "mutually exclusive"; if (logger) { logger->Log(LogDTO{.level = LogLevel::Error, .phase = PipelinePhase::Startup, @@ -127,10 +140,10 @@ std::optional ParseArguments( return std::nullopt; } - if (!use_mocked && model_path.empty()) { + if (selected_generator_count == 0) { const std::string msg = - "Invalid arguments: either --mocked or --model must be " - "specified"; + "Invalid arguments: exactly one of --mocked, --model, or " + "--openai must be specified"; if (logger) { logger->Log(LogDTO{.level = LogLevel::Error, .phase = PipelinePhase::Startup, @@ -141,8 +154,9 @@ std::optional ParseArguments( return std::nullopt; } - // Prompt directory is only meaningful for live inference — the mock - // generator has no use for it and should not require it to be present. + // Prompt directory is only meaningful for live inference (Llama or + // OpenAI) — the mock generator has no use for it and should not + // require it to be present. if (!use_mocked && options.pipeline.prompt_dir.empty()) { const std::string msg = "Invalid arguments: --prompt-dir is required when not using " @@ -157,8 +171,29 @@ std::optional ParseArguments( return std::nullopt; } - options.generator.use_mocked = use_mocked; + if (use_openai) { + const char* api_key_env = std::getenv("OPENAI_API_KEY"); + if (api_key_env == nullptr || api_key_env[0] == '\0') { + const std::string msg = + "Invalid arguments: the OPENAI_API_KEY environment " + "variable must be set when using --openai"; + if (logger) { + logger->Log({.level = LogLevel::Error, + .phase = PipelinePhase::Startup, + .message = msg}); + } else { + std::cerr << msg << '\n'; + } + return std::nullopt; + } + options.generator.openai_api_key = api_key_env; + } + + options.generator.mode = use_mocked ? GeneratorMode::kMock + : use_openai ? GeneratorMode::kOpenAI + : GeneratorMode::kLlama; options.generator.model_path = model_path; + options.generator.openai_model = var_map["openai-model"].as(); // Only populate sampling config when the user explicitly overrides at // least one value. Leaving it as std::nullopt lets LlamaGenerator fall @@ -172,10 +207,11 @@ std::optional ParseArguments( if (user_provided_sampling) { // Warn but do not fail — the run is still valid, the flags are just - // silently irrelevant when no model is loaded. - if (use_mocked) { + // silently irrelevant when no local model is loaded. + if (options.generator.mode != GeneratorMode::kLlama) { const std::string msg = - "Sampling parameters are ignored when using --mocked"; + "Sampling parameters are ignored unless using --model " + "(Llama)"; if (logger) { logger->Log(LogDTO{.level = LogLevel::Warn, .phase = PipelinePhase::Startup, diff --git a/tooling/pipeline/src/biergarten_pipeline_orchestrator/generate_breweries.cc b/tooling/pipeline/src/biergarten_pipeline_orchestrator/generate_breweries.cc index d2e273b1..2fa65463 100644 --- a/tooling/pipeline/src/biergarten_pipeline_orchestrator/generate_breweries.cc +++ b/tooling/pipeline/src/biergarten_pipeline_orchestrator/generate_breweries.cc @@ -4,12 +4,55 @@ */ #include +#include #include +#include #include +#include #include "biergarten_pipeline_orchestrator.h" #include "services/logging/logger.h" +struct Coords { + double longitude; + double latitude; +}; + +// @todo find a better home for this +namespace { +constexpr double kEarthRadiusKm = 6371.0; + +double DegreesToRadians(double degrees) { + return degrees * std::numbers::pi / 180.0; +} + +double RadiansToDegrees(double radians) { + return radians * 180.0 / std::numbers::pi; +} +} // namespace + +static Coords GetRandomCoordsWithinRange(const Coords& centre, + const int distance_km, + std::mt19937& rng) { + std::uniform_real_distribution angle_dist(0.0, + 2.0 * std::numbers::pi); + std::uniform_real_distribution unit_dist(0.0, 1.0); + + const double angle_radians = angle_dist(rng); + // sqrt() keeps points uniformly distributed over the disc's area rather + // than clustering near the centre. + const double radius_km = distance_km * std::sqrt(unit_dist(rng)); + + const double delta_latitude_degrees = + RadiansToDegrees(radius_km * std::cos(angle_radians) / kEarthRadiusKm); + const double delta_longitude_degrees = RadiansToDegrees( + radius_km * std::sin(angle_radians) / + (kEarthRadiusKm * std::cos(DegreesToRadians(centre.latitude)))); + + return Coords{.longitude = centre.longitude + delta_longitude_degrees, + .latitude = centre.latitude + delta_latitude_degrees}; +} + void BiergartenPipelineOrchestrator::GenerateBreweries( std::span cities) { logger_->Log({.level = LogLevel::Info, @@ -19,18 +62,28 @@ void BiergartenPipelineOrchestrator::GenerateBreweries( generated_breweries_.clear(); size_t skipped_count = 0; size_t export_failed_count = 0; + std::mt19937 rng(std::random_device{}()); const auto generate_record = - [this, &skipped_count]( + [this, &skipped_count, &rng]( const EnrichedCity& enriched_city) -> std::optional { try { const BreweryResult brewery = generator_->GenerateBrewery(enriched_city); const std::string postal_code = postal_code_service_->GeneratePostalCode(enriched_city.location); + + constexpr int kMaxDistanceFromCentreKm = 5; + const Coords city_centre{.longitude = enriched_city.location.longitude, + .latitude = enriched_city.location.latitude}; + const Coords brewery_coords = GetRandomCoordsWithinRange( + city_centre, kMaxDistanceFromCentreKm, rng); + return BreweryRecord{ .address = BreweryAddress{.city = enriched_city.location, - .postal_code = postal_code}, + .postal_code = postal_code, + .longitude = brewery_coords.longitude, + .latitude = brewery_coords.latitude}, .brewery = brewery}; } catch (const std::exception& e) { ++skipped_count; @@ -91,4 +144,4 @@ void BiergartenPipelineOrchestrator::GenerateBreweries( "brewery/breweries to SQLite", export_failed_count)}); } -} +} \ No newline at end of file diff --git a/tooling/pipeline/src/biergarten_pipeline_orchestrator/log_results.cc b/tooling/pipeline/src/biergarten_pipeline_orchestrator/log_results.cc index 5dd3c209..1b27752a 100644 --- a/tooling/pipeline/src/biergarten_pipeline_orchestrator/log_results.cc +++ b/tooling/pipeline/src/biergarten_pipeline_orchestrator/log_results.cc @@ -17,17 +17,19 @@ void BiergartenPipelineOrchestrator::LogResults() const { for (const auto& [address, brewery] : generated_breweries_) { const City& city = address.city; brewery_output.push_back(boost::json::object{ - {"name_en", brewery.name_en}, - {"description_en", brewery.description_en}, - {"name_local", brewery.name_local}, - {"description_local", brewery.description_local}, - {"address", boost::json::object{ - {"city", city.city}, - {"country", city.country}, - {"state_province", city.state_province}, - {"iso3166_2", city.iso3166_2}, - {"postal_code", address.postal_code}, - }}}); + {"name_en", brewery.name_en}, + {"description_en", brewery.description_en}, + {"name_local", brewery.name_local}, + {"description_local", brewery.description_local}, + {"address", boost::json::object{ + {"city", city.city}, + {"country", city.country}, + {"state_province", city.state_province}, + {"iso3166_2", city.iso3166_2}, + {"postal_code", address.postal_code}, + {"longitude", address.longitude}, + {"latitude", address.latitude}, + }}}); } std::ostringstream brewery_oss; @@ -40,14 +42,14 @@ void BiergartenPipelineOrchestrator::LogResults() const { for (const auto& generated_user : generated_users_) { user_output.push_back(boost::json::object{ - {"first_name", generated_user.user.first_name}, - {"last_name", generated_user.user.last_name}, - {"gender", generated_user.user.gender}, - {"username", generated_user.user.username}, - {"bio", generated_user.user.bio}, - {"activity_weight", generated_user.user.activity_weight}, - {"email", generated_user.email}, - {"date_of_birth", generated_user.date_of_birth}, + {"first_name", generated_user.user.first_name}, + {"last_name", generated_user.user.last_name}, + {"gender", generated_user.user.gender}, + {"username", generated_user.user.username}, + {"bio", generated_user.user.bio}, + {"activity_weight", generated_user.user.activity_weight}, + {"email", generated_user.email}, + {"date_of_birth", generated_user.date_of_birth}, }); } @@ -56,4 +58,4 @@ void BiergartenPipelineOrchestrator::LogResults() const { logger_->Log({.level = LogLevel::Info, .phase = PipelinePhase::Teardown, .message = user_oss.str()}); -} +} \ No newline at end of file diff --git a/tooling/pipeline/src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc b/tooling/pipeline/src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc index 95e9c096..4c5d5ada 100644 --- a/tooling/pipeline/src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc +++ b/tooling/pipeline/src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc @@ -39,11 +39,10 @@ std::vector BiergartenPipelineOrchestrator::QueryLocations() { logger_->Log({.level = LogLevel::Info, .phase = PipelinePhase::Startup, - .message = std::format(" Locations available: {}", - all_locations.size())}); - logger_->Log({.level = LogLevel::Info, - .phase = PipelinePhase::Startup, - .message = std::format(" Sampled locations: {}", - sampled_locations.size())}); + .message = std::format( + " Locations available: {}\n Sampled Locations: {}", + all_locations.size(), sampled_locations.size())}); + + return sampled_locations; -} +} \ No newline at end of file diff --git a/tooling/pipeline/src/data_generation/generation_json_validation.cc b/tooling/pipeline/src/data_generation/generation_json_validation.cc new file mode 100644 index 00000000..42344f16 --- /dev/null +++ b/tooling/pipeline/src/data_generation/generation_json_validation.cc @@ -0,0 +1,236 @@ +/** + * @file data_generation/generation_json_validation.cc + * @brief Generator-agnostic prompt-context preparation and structured JSON + * validation, shared across all DataGenerator implementations. + */ + +#include "data_generation/generation_json_validation.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { +/** + * String trimming: removes leading and trailing whitespace + */ +std::string Trim(std::string_view value) { + constexpr std::string_view whitespace = " \t\n\r\f\v"; + const size_t first_index = value.find_first_not_of(whitespace); + if (first_index == std::string_view::npos) { + return {}; + } + + const size_t last_index = value.find_last_not_of(whitespace); + return std::string(value.substr(first_index, last_index - first_index + 1)); +} + +/** + * Normalize whitespace: collapses multiple spaces/tabs/newlines into single + * spaces + */ +std::string CondenseWhitespace(std::string_view text) { + std::string out; + out.reserve(text.size()); + + bool pending_space = false; + for (const char chr : text) { + if (std::isspace(static_cast(chr)) != 0) { + if (!out.empty()) { + pending_space = true; + } + continue; + } + + if (pending_space) { + out.push_back(' '); + pending_space = false; + } + out.push_back(chr); + } + + return out; +} + +// Guard against truncating in the first half of the string. +// This preserves the critical opening content and avoids cutting critical +// context words early in the region description. +constexpr size_t kTruncationGuardDivisor = 2; + +bool ReadRequiredTrimmedStringField(const boost::json::object& obj, + std::string_view key, std::string& out, + std::string* error_out) { + const boost::json::value* field = obj.if_contains(key); + if (field == nullptr || !field->is_string()) { + if (error_out != nullptr) { + *error_out = + std::format("Missing or invalid string field: {}", key); + } + return false; + } + + const auto& string_value = field->as_string(); + out = Trim(std::string_view(string_value.data(), string_value.size())); + if (out.empty() && error_out != nullptr) { + *error_out = std::format("Field must not be empty: {}", key); + } + return !out.empty(); +} + +bool HasSchemaPlaceholder(std::span values) { + for (const std::string* value : values) { + std::string lowered = *value; + std::ranges::transform( + lowered, lowered.begin(), [](const unsigned char character) { + return static_cast(std::tolower(character)); + }); + + if (lowered == "string") { + return true; + } + } + + return false; +} +} // namespace + +/** + * Truncate region context to fit within max length while preserving word + * boundaries + */ +std::string PrepareRegionContext(std::string_view region_context, + const size_t max_chars) { + std::string normalized = CondenseWhitespace(region_context); + if (normalized.size() <= max_chars) { + return normalized; + } + + normalized.resize(max_chars); + const size_t last_space = normalized.find_last_of(' '); + if (last_space != std::string::npos && + last_space > max_chars / kTruncationGuardDivisor) { + normalized.resize(last_space); + } + + normalized += "..."; + return normalized; +} + +std::optional ValidateBreweryJson(const std::string& raw, + BreweryResult& brewery_out) { + boost::system::error_code error_code; + const std::string_view raw_view(raw); + const size_t opening_brace = raw_view.find('{'); + if (opening_brace == std::string_view::npos) { + return "JSON parse error: missing opening brace '{'"; + } + + const std::string_view json_payload = raw_view.substr(opening_brace); + boost::json::value json_value = boost::json::parse(json_payload, error_code); + if (error_code) { + return "JSON parse error: " + error_code.message(); + } + + if (!json_value.is_object()) { + return "JSON root must be an object"; + } + + const auto& obj = json_value.get_object(); + if (obj.size() != 4) { + return "JSON object must contain exactly four keys"; + } + + std::string validation_error; + if (!ReadRequiredTrimmedStringField(obj, "name_en", brewery_out.name_en, + &validation_error)) { + return validation_error; + } + + if (!ReadRequiredTrimmedStringField(obj, "description_en", + brewery_out.description_en, + &validation_error)) { + return validation_error; + } + + if (!ReadRequiredTrimmedStringField( + obj, "name_local", brewery_out.name_local, &validation_error)) { + return validation_error; + } + + if (!ReadRequiredTrimmedStringField(obj, "description_local", + brewery_out.description_local, + &validation_error)) { + return validation_error; + } + + const std::array schema_placeholders = { + &brewery_out.name_en, &brewery_out.description_en, + &brewery_out.name_local, &brewery_out.description_local}; + if (HasSchemaPlaceholder(schema_placeholders)) { + return "JSON appears to be a schema placeholder, not content"; + } + + return std::nullopt; +} + +std::optional ValidateUserJson(const std::string& raw, + UserResult& user_out) { + boost::system::error_code error_code; + const std::string_view raw_view(raw); + const size_t opening_brace = raw_view.find('{'); + if (opening_brace == std::string_view::npos) { + return "JSON parse error: missing opening brace '{'"; + } + + const std::string_view json_payload = raw_view.substr(opening_brace); + boost::json::value json_value = boost::json::parse(json_payload, error_code); + if (error_code) { + return "JSON parse error: " + error_code.message(); + } + + if (!json_value.is_object()) { + return "JSON root must be an object"; + } + + const auto& obj = json_value.get_object(); + if (obj.size() != 3) { + return "JSON object must contain exactly three keys"; + } + + std::string validation_error; + if (!ReadRequiredTrimmedStringField(obj, "username", user_out.username, + &validation_error)) { + return validation_error; + } + + if (!ReadRequiredTrimmedStringField(obj, "bio", user_out.bio, + &validation_error)) { + return validation_error; + } + + const boost::json::value* activity_weight_field = + obj.if_contains("activity_weight"); + if (activity_weight_field == nullptr || + !activity_weight_field->is_number()) { + return "Missing or invalid numeric field: activity_weight"; + } + + const double activity_weight = activity_weight_field->to_number(); + if (activity_weight < 0.0 || activity_weight > 1.0) { + return "activity_weight must be between 0 and 1"; + } + user_out.activity_weight = static_cast(activity_weight); + + const std::array schema_placeholders = {&user_out.username, &user_out.bio}; + if (HasSchemaPlaceholder(schema_placeholders)) { + return "JSON appears to be a schema placeholder, not content"; + } + + return std::nullopt; +} diff --git a/tooling/pipeline/src/data_generation/llama/generate_brewery.cc b/tooling/pipeline/src/data_generation/llama/generate_brewery.cc index d110e5f3..a2b26113 100644 --- a/tooling/pipeline/src/data_generation/llama/generate_brewery.cc +++ b/tooling/pipeline/src/data_generation/llama/generate_brewery.cc @@ -12,9 +12,9 @@ #include #include +#include "data_generation/generation_json_validation.h" #include "data_generation/json_grammars.h" #include "data_generation/llama_generator.h" -#include "data_generation/llama_generator_helpers.h" static std::string FormatLocalLanguageCodes( const std::vector& codes) { diff --git a/tooling/pipeline/src/data_generation/llama/generate_user.cc b/tooling/pipeline/src/data_generation/llama/generate_user.cc index 8eb68c6b..4bafedf5 100644 --- a/tooling/pipeline/src/data_generation/llama/generate_user.cc +++ b/tooling/pipeline/src/data_generation/llama/generate_user.cc @@ -10,9 +10,9 @@ #include #include +#include "data_generation/generation_json_validation.h" #include "data_generation/json_grammars.h" #include "data_generation/llama_generator.h" -#include "data_generation/llama_generator_helpers.h" static constexpr int kUserInitialMaxTokens = 1200; diff --git a/tooling/pipeline/src/data_generation/llama/helpers.cc b/tooling/pipeline/src/data_generation/llama/helpers.cc index 09ea5505..82260d94 100644 --- a/tooling/pipeline/src/data_generation/llama/helpers.cc +++ b/tooling/pipeline/src/data_generation/llama/helpers.cc @@ -1,123 +1,17 @@ /** * @file data_generation/llama/helpers.cc - * @brief Provides prompt formatting, whitespace normalization, response - * parsing, token decoding, and JSON validation helpers for Llama modules. + * @brief Token decoding helper for LlamaGenerator modules. */ #include -#include #include -#include -#include -#include -#include #include #include -#include #include #include "data_generation/llama_generator_helpers.h" -namespace { -/** - * String trimming: removes leading and trailing whitespace - */ -std::string Trim(std::string_view value) { - constexpr std::string_view whitespace = " \t\n\r\f\v"; - const size_t first_index = value.find_first_not_of(whitespace); - if (first_index == std::string_view::npos) { - return {}; - } - - const size_t last_index = value.find_last_not_of(whitespace); - return std::string(value.substr(first_index, last_index - first_index + 1)); -} - -/** - * Normalize whitespace: collapses multiple spaces/tabs/newlines into single - * spaces - */ -std::string CondenseWhitespace(std::string_view text) { - std::string out; - out.reserve(text.size()); - - bool pending_space = false; - for (const char chr : text) { - if (std::isspace(static_cast(chr)) != 0) { - if (!out.empty()) { - pending_space = true; - } - continue; - } - - if (pending_space) { - out.push_back(' '); - pending_space = false; - } - out.push_back(chr); - } - - return out; -} - -// Guard against truncating in the first half of the string. -// This preserves the critical opening content and avoids cutting critical -// context words early in the region description. -constexpr size_t kTruncationGuardDivisor = 2; - -bool ReadRequiredTrimmedStringField(const boost::json::object& obj, - std::string_view key, std::string& out, - std::string* error_out) { - const boost::json::value* field = obj.if_contains(key); - if (field == nullptr || !field->is_string()) { - return false; - } - - const auto& string_value = field->as_string(); - out = Trim(std::string_view(string_value.data(), string_value.size())); - return !out.empty(); -} - -bool HasSchemaPlaceholder(std::span values) { - for (const std::string* value : values) { - std::string lowered = *value; - std::ranges::transform( - lowered, lowered.begin(), [](const unsigned char character) { - return static_cast(std::tolower(character)); - }); - - if (lowered == "string") { - return true; - } - } - - return false; -} -} // namespace - -/** - * Truncate region context to fit within max length while preserving word - * boundaries - */ -std::string PrepareRegionContext(std::string_view region_context, - const size_t max_chars) { - std::string normalized = CondenseWhitespace(region_context); - if (normalized.size() <= max_chars) { - return normalized; - } - - normalized.resize(max_chars); - const size_t last_space = normalized.find_last_of(' '); - if (last_space != std::string::npos && - last_space > max_chars / kTruncationGuardDivisor) { - normalized.resize(last_space); - } - - normalized += "..."; - return normalized; -} - void AppendTokenPiece(const llama_vocab* vocab, llama_token token, std::string& output) { constexpr size_t initial_buffer_size = 256; @@ -153,116 +47,3 @@ void AppendTokenPiece(const llama_vocab* vocab, llama_token token, throw std::runtime_error( "LlamaGenerator: failed to decode sampled token piece"); } - -std::optional ValidateBreweryJson(const std::string& raw, - BreweryResult& brewery_out) { - boost::system::error_code error_code; - const std::string_view raw_view(raw); - const size_t opening_brace = raw_view.find('{'); - if (opening_brace == std::string_view::npos) { - return "JSON parse error: missing opening brace '{'"; - } - - const std::string_view json_payload = raw_view.substr(opening_brace); - boost::json::value json_value = boost::json::parse(json_payload, error_code); - if (error_code) { - return "JSON parse error: " + error_code.message(); - } - - if (!json_value.is_object()) { - return "JSON root must be an object"; - } - - const auto& obj = json_value.get_object(); - if (obj.size() != 4) { - return "JSON object must contain exactly four keys"; - } - - std::string validation_error; - if (!ReadRequiredTrimmedStringField(obj, "name_en", brewery_out.name_en, - &validation_error)) { - return validation_error; - } - - if (!ReadRequiredTrimmedStringField(obj, "description_en", - brewery_out.description_en, - &validation_error)) { - return validation_error; - } - - if (!ReadRequiredTrimmedStringField( - obj, "name_local", brewery_out.name_local, &validation_error)) { - return validation_error; - } - - if (!ReadRequiredTrimmedStringField(obj, "description_local", - brewery_out.description_local, - &validation_error)) { - return validation_error; - } - - const std::array schema_placeholders = { - &brewery_out.name_en, &brewery_out.description_en, - &brewery_out.name_local, &brewery_out.description_local}; - if (HasSchemaPlaceholder(schema_placeholders)) { - return "JSON appears to be a schema placeholder, not content"; - } - - return std::nullopt; -} - -std::optional ValidateUserJson(const std::string& raw, - UserResult& user_out) { - boost::system::error_code error_code; - const std::string_view raw_view(raw); - const size_t opening_brace = raw_view.find('{'); - if (opening_brace == std::string_view::npos) { - return "JSON parse error: missing opening brace '{'"; - } - - const std::string_view json_payload = raw_view.substr(opening_brace); - boost::json::value json_value = boost::json::parse(json_payload, error_code); - if (error_code) { - return "JSON parse error: " + error_code.message(); - } - - if (!json_value.is_object()) { - return "JSON root must be an object"; - } - - const auto& obj = json_value.get_object(); - if (obj.size() != 3) { - return "JSON object must contain exactly three keys"; - } - - std::string validation_error; - if (!ReadRequiredTrimmedStringField(obj, "username", user_out.username, - &validation_error)) { - return validation_error; - } - - if (!ReadRequiredTrimmedStringField(obj, "bio", user_out.bio, - &validation_error)) { - return validation_error; - } - - const boost::json::value* activity_weight_field = - obj.if_contains("activity_weight"); - if (activity_weight_field == nullptr || - !activity_weight_field->is_number()) { - return "Missing or invalid numeric field: activity_weight"; - } - - const double activity_weight = activity_weight_field->to_number(); - if (activity_weight < 0.0 || activity_weight > 1.0) { - return "activity_weight must be between 0 and 1"; - } - user_out.activity_weight = static_cast(activity_weight); - - const std::array schema_placeholders = {&user_out.username, &user_out.bio}; - if (HasSchemaPlaceholder(schema_placeholders)) { - return "JSON appears to be a schema placeholder, not content"; - } - - return std::nullopt; -} diff --git a/tooling/pipeline/src/data_generation/openai/call_chat_completions_api.cc b/tooling/pipeline/src/data_generation/openai/call_chat_completions_api.cc new file mode 100644 index 00000000..f878616f --- /dev/null +++ b/tooling/pipeline/src/data_generation/openai/call_chat_completions_api.cc @@ -0,0 +1,182 @@ +/** + * @file data_generation/openai/call_chat_completions_api.cc + * @brief OpenAIGenerator::CallChatCompletionsApi() -- builds and sends a + * single OpenAI Chat Completions request with a Structured Outputs JSON + * schema, and extracts the resulting message content. + * + * API docs last accessed 2026-08-23. + * Docs: https://platform.openai.com/docs/api-reference/chat/create + * Structured Outputs: https://platform.openai.com/docs/guides/structured-outputs + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "data_generation/openai_generator.h" + +namespace { +// https://platform.openai.com/docs/api-reference/chat/create +constexpr std::string_view kChatCompletionsUrl = + "https://api.openai.com/v1/chat/completions"; +} // namespace + +/** + * @brief Sends a single Chat Completions request constrained by a JSON + * Schema and returns the assistant message's text content. + * + * Request/response fields follow the Chat Completions API reference: + * https://platform.openai.com/docs/api-reference/chat/create + * Structured Outputs (response_format, strict mode, refusal field): + * https://platform.openai.com/docs/guides/structured-outputs + * + * @param system_prompt Text sent as the "system" role message. + * @param user_prompt Text sent as the "user" role message. + * @param json_schema JSON Schema the response must conform to. + * @param schema_name Identifier for response_format.json_schema.name. + * @param max_tokens Value sent as "max_completion_tokens". + * @return The raw JSON string from choices[0].message.content. + * @throws std::runtime_error on invalid schema, API error, malformed + * response, model refusal, or missing content. + */ +std::string OpenAIGenerator::CallChatCompletionsApi( + const std::string& system_prompt, const std::string& user_prompt, + std::string_view json_schema, std::string_view schema_name, + int max_tokens) { + + boost::system::error_code schema_error; + boost::json::value schema_value = + boost::json::parse(json_schema, schema_error); + + bool schema_is_invalid = static_cast(schema_error); + if (schema_is_invalid) { + throw std::runtime_error( + "OpenAIGenerator: invalid embedded JSON schema: " + + schema_error.message()); + } + + // response_format: https://platform.openai.com/docs/guides/structured-outputs + boost::json::object json_schema_wrapper; + json_schema_wrapper["name"] = std::string(schema_name); + json_schema_wrapper["strict"] = true; + json_schema_wrapper["schema"] = std::move(schema_value); + + boost::json::object response_format; + response_format["type"] = "json_schema"; + response_format["json_schema"] = std::move(json_schema_wrapper); + + boost::json::object system_message; + system_message["role"] = "system"; + system_message["content"] = system_prompt; + + boost::json::object user_message; + user_message["role"] = "user"; + user_message["content"] = user_prompt; + + boost::json::array messages; + messages.push_back(std::move(system_message)); + messages.push_back(std::move(user_message)); + + boost::json::object request; + request["model"] = model_; + request["max_completion_tokens"] = max_tokens; + request["messages"] = std::move(messages); + request["response_format"] = std::move(response_format); + + const std::string body = boost::json::serialize(request); + + const std::vector> headers = { + {"Authorization", std::format("Bearer {}", api_key_)}, + }; + + const std::string response_body = + web_client_->Post(std::string(kChatCompletionsUrl), body, headers); + + boost::system::error_code response_error; + boost::json::value response_value = + boost::json::parse(response_body, response_error); + + bool response_body_is_invalid = static_cast(response_error); + if (response_body_is_invalid) { + throw std::runtime_error( + "OpenAIGenerator: failed to parse API response: " + + response_error.message()); + } + + bool response_root_is_not_object = !response_value.is_object(); + if (response_root_is_not_object) { + throw std::runtime_error( + "OpenAIGenerator: API response root is not an object"); + } + const auto& response_obj = response_value.get_object(); + + // Error object: https://platform.openai.com/docs/api-reference/chat/create + const boost::json::value* error_field = response_obj.if_contains("error"); + + bool top_level_error_occurred = + error_field != nullptr && error_field->is_object(); + if (top_level_error_occurred) { + std::string error_message = "unknown error"; + if (const boost::json::value* message_field = + error_field->get_object().if_contains("message"); + message_field != nullptr && message_field->is_string()) { + error_message = std::string(message_field->as_string()); + } + throw std::runtime_error( + std::format("OpenAIGenerator: API request failed: {}", error_message)); + } + + const boost::json::value* choices_field = response_obj. + if_contains("choices"); + bool choices_field_missing_or_invalid = + choices_field == nullptr || !choices_field->is_array() || + choices_field->get_array().empty(); + + if (choices_field_missing_or_invalid) { + throw std::runtime_error( + "OpenAIGenerator: API response missing choices array"); + } + + const boost::json::value& first_choice = choices_field->get_array().front(); + bool first_choice_is_not_object = !first_choice.is_object(); + if (first_choice_is_not_object) { + throw std::runtime_error( + "OpenAIGenerator: API response choice is not an object"); + } + const auto& choice_obj = first_choice.get_object(); + + const boost::json::value* message_field = choice_obj.if_contains("message"); + bool message_field_missing_or_invalid = + message_field == nullptr || !message_field->is_object(); + if (message_field_missing_or_invalid) { + throw std::runtime_error( + "OpenAIGenerator: API response choice missing message"); + } + const auto& message_obj = message_field->get_object(); + + // Refusal field: https://platform.openai.com/docs/guides/structured-outputs + const boost::json::value* refusal_field = message_obj.if_contains("refusal"); + bool model_refused_request = + refusal_field != nullptr && refusal_field->is_string() && + !refusal_field->as_string().empty(); + if (model_refused_request) { + throw std::runtime_error(std::format( + "OpenAIGenerator: request refused by safety policies: {}", + std::string(refusal_field->as_string()))); + } + + const boost::json::value* content_field = message_obj.if_contains("content"); + bool content_field_missing_or_invalid = + content_field == nullptr || !content_field->is_string(); + if (content_field_missing_or_invalid) { + throw std::runtime_error( + "OpenAIGenerator: API response message had no text content"); + } + + return std::string(content_field->as_string()); +} diff --git a/tooling/pipeline/src/data_generation/openai/generate_brewery.cc b/tooling/pipeline/src/data_generation/openai/generate_brewery.cc new file mode 100644 index 00000000..3f39fe87 --- /dev/null +++ b/tooling/pipeline/src/data_generation/openai/generate_brewery.cc @@ -0,0 +1,132 @@ +/** + * @file data_generation/openai/generate_brewery.cc + * @brief Builds brewery prompts with regional context, calls the OpenAI + * Chat Completions API, and validates structured JSON output for brewery + * records. + */ + +#include +#include +#include +#include +#include + +#include "data_generation/generation_json_validation.h" +#include "data_generation/openai_generator.h" +#include "data_generation/openai_json_schemas.h" + +namespace { + +std::string FormatLocalLanguageCodes( + const std::vector& codes) { + if (codes.empty()) { + return "Not provided"; + } + + std::string formatted; + for (const std::string& code : codes) { + if (!formatted.empty()) { + formatted += ", "; + } + formatted += code; + } + + return formatted; +} + +constexpr int kBreweryMaxTokens = 2800; + +// Structured outputs already guarantee schema-valid JSON, so retries here +// are only for transient failures or the model returning placeholder text +// -- not for malformed JSON shape, unlike LlamaGenerator's grammar-assisted +// retry loop. +constexpr int kMaxAttempts = 2; + +} // namespace + +BreweryResult OpenAIGenerator::GenerateBrewery( + const EnrichedCity& enriched_city) { + const City& location = enriched_city.location; + const std::string& region_context = enriched_city.region_context; + const std::string safe_region_context = PrepareRegionContext(region_context); + + const std::string local_language_codes = + FormatLocalLanguageCodes(location.local_languages); + + /** + * Load brewery system prompt via the injected prompt directory. The + * same file LlamaGenerator uses -- the prompt content is backend + * agnostic. + */ + const std::string system_prompt = + prompt_directory_->Load("BREWERY_GENERATION"); + + std::string user_prompt = std::format( + "## CITY:\n{}\n\n## COUNTRY:\n{}\n\n## LOCAL LANGUAGE CODES:\n{}\n\n## " + "CONTEXT:\n{}", + location.city, location.country, local_language_codes, + safe_region_context); + + const std::string retry_location = + std::format("City: {}, {}\nLocal language codes: {}", location.city, + location.country, local_language_codes); + + std::string raw; + std::string last_error; + + for (int attempt = 0; attempt < kMaxAttempts; ++attempt) { + raw = CallChatCompletionsApi(system_prompt, user_prompt, + kBreweryJsonSchema, "brewery_result", + kBreweryMaxTokens); + if (logger_) { + logger_->Log({.level = LogLevel::Debug, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format( + "OpenAIGenerator: raw output (attempt {}): {}", + attempt + 1, raw)}); + } + + BreweryResult brewery; + const std::optional validation_error = + ValidateBreweryJson(raw, brewery); + + if (!validation_error.has_value()) { + if (logger_) { + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format( + "OpenAIGenerator: successfully generated " + "brewery data on attempt {}", + attempt + 1)}); + } + + return brewery; + } + + last_error = *validation_error; + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format( + "OpenAIGenerator: malformed brewery JSON (attempt {}): {}", + attempt + 1, *validation_error)}); + } + + user_prompt = std::format( + "Your previous response was invalid. Error: {}\nReturn real " + "content matching the required schema -- do not return " + "placeholder values.\n\n{}", + *validation_error, retry_location); + } + + if (logger_) { + logger_->Log({.level = LogLevel::Error, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format( + "OpenAIGenerator: malformed brewery " + "response after {} attempts: {}", + kMaxAttempts, last_error.empty() ? raw : last_error)}); + } + throw std::runtime_error("OpenAIGenerator: malformed brewery response"); +} diff --git a/tooling/pipeline/src/data_generation/openai/generate_user.cc b/tooling/pipeline/src/data_generation/openai/generate_user.cc new file mode 100644 index 00000000..ab792104 --- /dev/null +++ b/tooling/pipeline/src/data_generation/openai/generate_user.cc @@ -0,0 +1,114 @@ +/** + * @file data_generation/openai/generate_user.cc + * @brief Builds persona/name-grounded user prompts, calls the OpenAI Chat + * Completions API, and validates structured JSON output for user records. + */ + +#include +#include +#include +#include + +#include "data_generation/generation_json_validation.h" +#include "data_generation/openai_generator.h" +#include "data_generation/openai_json_schemas.h" + +namespace { +constexpr int kUserMaxTokens = 1200; + +// Structured outputs already guarantee schema-valid JSON, so retries here +// are only for transient failures or placeholder text -- not JSON shape. +constexpr int kMaxAttempts = 2; +} // namespace + +UserResult OpenAIGenerator::GenerateUser(const EnrichedCity& city, + const UserPersona& persona, + const Name& name) { + std::string style_affinities; + for (const std::string& style : persona.style_affinities) { + if (!style_affinities.empty()) { + style_affinities += ", "; + } + style_affinities += style; + } + + const std::string system_prompt = prompt_directory_->Load("USER_GENERATION"); + + std::string user_prompt = std::format( + "## NAME:\n\n{} {}\n\n" + "## GENDER:\n\n{}\n\n" + "## CITY:\n\n{}\n\n" + "## COUNTRY:\n\n{}\n\n" + "## PERSONA:\n\n{}\n\n" + "## PERSONA DESCRIPTION:\n\n{}\n\n" + "## STYLE AFFINITIES:\n\n{}", + name.first_name, name.last_name, name.gender, city.location.city, + city.location.country, persona.name, persona.description, + style_affinities); + + const std::string retry_context = std::format( + "Name: {} {}\nCity: {}, {}\nPersona: {}", name.first_name, + name.last_name, city.location.city, city.location.country, + persona.name); + + std::string raw; + std::string last_error; + + for (int attempt = 0; attempt < kMaxAttempts; ++attempt) { + raw = CallChatCompletionsApi(system_prompt, user_prompt, kUserJsonSchema, + "user_result", kUserMaxTokens); + if (logger_) { + logger_->Log({.level = LogLevel::Debug, + .phase = PipelinePhase::UserGeneration, + .message = std::format( + "OpenAIGenerator: raw output (attempt {}): {}", + attempt + 1, raw)}); + } + + UserResult user; + const std::optional validation_error = + ValidateUserJson(raw, user); + + if (!validation_error.has_value()) { + if (logger_) { + logger_->Log( + {.level = LogLevel::Info, + .phase = PipelinePhase::UserGeneration, + .message = std::format("OpenAIGenerator: successfully " + "generated user data on attempt {}", + attempt + 1)}); + } + + user.first_name = name.first_name; + user.last_name = name.last_name; + user.gender = name.gender; + return user; + } + + last_error = *validation_error; + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = std::format( + "OpenAIGenerator: malformed user JSON (attempt {}): {}", + attempt + 1, *validation_error)}); + } + + user_prompt = std::format( + "Your previous response was invalid. Error: {}\nReturn real " + "content matching the required schema -- do not return " + "placeholder values.\n\n{}", + *validation_error, retry_context); + } + + if (logger_) { + logger_->Log({.level = LogLevel::Error, + .phase = PipelinePhase::UserGeneration, + .message = std::format( + "OpenAIGenerator: malformed user response " + "after {} attempts: {}", + kMaxAttempts, last_error.empty() ? raw : last_error)}); + } + throw std::runtime_error("OpenAIGenerator: malformed user response"); +} diff --git a/tooling/pipeline/src/data_generation/openai/openai_generator.cc b/tooling/pipeline/src/data_generation/openai/openai_generator.cc new file mode 100644 index 00000000..f194a630 --- /dev/null +++ b/tooling/pipeline/src/data_generation/openai/openai_generator.cc @@ -0,0 +1,20 @@ +/** + * @file data_generation/openai/openai_generator.cc + * @brief OpenAIGenerator constructor. + */ + +#include +#include +#include + +#include "data_generation/openai_generator.h" + +OpenAIGenerator::OpenAIGenerator( + std::string api_key, std::string model, std::shared_ptr logger, + std::unique_ptr prompt_directory, + std::unique_ptr web_client) + : api_key_(std::move(api_key)), + model_(std::move(model)), + logger_(std::move(logger)), + prompt_directory_(std::move(prompt_directory)), + web_client_(std::move(web_client)) {} diff --git a/tooling/pipeline/src/main.cc b/tooling/pipeline/src/main.cc index 5154058a..32b01709 100644 --- a/tooling/pipeline/src/main.cc +++ b/tooling/pipeline/src/main.cc @@ -67,7 +67,7 @@ int main(const int argc, char** argv) { std::unique_ptr prompt_directory; - if (!options.generator.use_mocked) { + if (options.generator.mode != GeneratorMode::kMock) { try { prompt_directory = std::make_unique( options.pipeline.prompt_dir, log_producer); @@ -90,7 +90,7 @@ int main(const int argc, char** argv) { di::bind().to( [options, &log_producer]() -> std::unique_ptr { - if (options.generator.use_mocked) { + if (options.generator.mode == GeneratorMode::kMock) { log_producer->Log( {.level = LogLevel::Info, .phase = PipelinePhase::Startup, @@ -110,13 +110,13 @@ int main(const int argc, char** argv) { di::bind().to( [options, &log_producer]() -> std::unique_ptr { - if (options.generator.use_mocked) { - log_producer->Log({.level = LogLevel::Info, - .phase = PipelinePhase::Startup, - .message = "Curated data: mock"}); - - return std::make_unique(); - } + // if (options.generator.mode == GeneratorMode::kMock) { + // log_producer->Log({.level = LogLevel::Info, + // .phase = PipelinePhase::Startup, + // .message = "Curated data: mock"}); + // + // return std::make_unique(); + // } log_producer->Log({.level = LogLevel::Info, .phase = PipelinePhase::Startup, @@ -131,13 +131,12 @@ int main(const int argc, char** argv) { }); }), di::bind().to([options, log_producer] { - if (options.generator.use_mocked) { - { - log_producer->Log( - {.level = LogLevel::Info, - .phase = PipelinePhase::Startup, - .message = "Prompt formatter: none (mock mode)"}); - } + if (options.generator.mode != GeneratorMode::kLlama) { + log_producer->Log( + {.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "Prompt formatter: none (not needed outside " + "Llama mode)"}); return std::unique_ptr(nullptr); } { @@ -151,7 +150,7 @@ int main(const int argc, char** argv) { std::make_unique()); }), di::bind().to([options, log_producer] { - if (options.generator.use_mocked) { + if (options.generator.mode == GeneratorMode::kMock) { { log_producer->Log( {.level = LogLevel::Info, @@ -171,7 +170,7 @@ int main(const int argc, char** argv) { di::bind().to( [options, &log_producer]( const auto& inj) -> std::unique_ptr { - if (options.generator.use_mocked) { + if (options.generator.mode == GeneratorMode::kMock) { log_producer->Log({.level = LogLevel::Info, .phase = PipelinePhase::Startup, .message = "Enrichment: mock"}); @@ -191,7 +190,7 @@ int main(const int argc, char** argv) { [&options, &model_path, &sampling, &prompt_directory, &log_producer]( const auto& inj) -> std::unique_ptr { - if (options.generator.use_mocked) { + if (options.generator.mode == GeneratorMode::kMock) { log_producer->Log({.level = LogLevel::Info, .phase = PipelinePhase::Startup, .message = "Generator: mock"}); @@ -199,10 +198,25 @@ int main(const int argc, char** argv) { return std::make_unique(); } + if (options.generator.mode == GeneratorMode::kOpenAI) { + log_producer->Log( + {.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = std::format( + "Generator: OpenAIGenerator | model={}", + options.generator.openai_model)}); + + return std::make_unique( + options.generator.openai_api_key, + options.generator.openai_model, log_producer, + std::move(prompt_directory), + inj.template create>()); + } + #ifdef BIERGARTEN_MOCK_ONLY - // Unreachable: ParseArguments requires --mocked when - // BIERGARTEN_MOCK_ONLY is compiled in, so LlamaGenerator is - // never constructed and is not linked into this binary. + // Only reachable when mode == Llama: Mock and OpenAI are + // handled above and don't need llama.cpp, but LlamaGenerator + // itself is not linked into a BIERGARTEN_MOCK_ONLY build. throw std::runtime_error( "LlamaGenerator is unavailable in a BIERGARTEN_MOCK_ONLY " "build"); diff --git a/tooling/pipeline/src/services/curated_data/curated_json_data_service.cc b/tooling/pipeline/src/services/curated_data/curated_json_data_service.cc index c85a7820..eb17df45 100644 --- a/tooling/pipeline/src/services/curated_data/curated_json_data_service.cc +++ b/tooling/pipeline/src/services/curated_data/curated_json_data_service.cc @@ -30,6 +30,16 @@ static std::string ReadRequiredString(const boost::json::object& object, return std::string(text); } +static double ReadRequiredDouble(const boost::json::object& object, + const char* key) { + const boost::json::value* value = object.if_contains(key); + if (value == nullptr || !value->is_double()) { + throw std::runtime_error( + std::format("Missing or invalid double field: {}", key)); + } + return value->as_double(); +} + static std::vector ReadRequiredStringArray( const boost::json::object& object, const char* key) { const boost::json::value* value = object.if_contains(key); @@ -67,7 +77,6 @@ static PostalCodeSpec ReadPostalCodeSpec(const boost::json::object& object) { } namespace { - boost::json::value ParseJsonFile(const std::filesystem::path& filepath, const char* what) { std::ifstream input(filepath); @@ -108,7 +117,6 @@ std::string ReadFirstOfStringArray(const boost::json::object& object, throw std::runtime_error( std::format("Missing or invalid string array field: {}", key)); } - } // namespace CuratedJsonDataService::CuratedJsonDataService(CuratedDataFilePaths filepaths) @@ -144,6 +152,8 @@ const LocationsList& CuratedJsonDataService::LoadLocations() { .iso3166_2 = ReadRequiredString(object, "iso3166_2"), .country = ReadRequiredString(object, "country"), .iso3166_1 = ReadRequiredString(object, "iso3166_1"), + .latitude = ReadRequiredDouble(object, "latitude"), + .longitude = ReadRequiredDouble(object, "longitude"), .local_languages = ReadRequiredStringArray(object, "local_languages"), .postal_code = ReadPostalCodeSpec(object), }); @@ -267,4 +277,4 @@ const SurnamesByCountryMap& CuratedJsonDataService::LoadSurnamesByCountry() { cache_.surnames_by_country = std::move(surnames_by_country); return cache_.surnames_by_country; -} +} \ No newline at end of file diff --git a/tooling/pipeline/src/services/sqlite/process_record.cc b/tooling/pipeline/src/services/sqlite/process_record.cc index 1c860aee..964892a7 100644 --- a/tooling/pipeline/src/services/sqlite/process_record.cc +++ b/tooling/pipeline/src/services/sqlite/process_record.cc @@ -66,6 +66,18 @@ sqlite3_int64 SqliteExportService::ResolveCityId(const City& city) { .index = sqlite_export_service_internal::kCityIso31661BindIndex, .value = city.iso3166_1, .action = "Failed to bind SQLite city ISO 3166-1 code"}); + sqlite_export_service_internal::Bind( + insert_city_stmt_, + sqlite_export_service_internal::BoundParam{ + .index = sqlite_export_service_internal::kCityLongitudeBindIndex, + .value = city.longitude, + .action = "Failed to bind SQLite city longitude"}); + sqlite_export_service_internal::Bind( + insert_city_stmt_, + sqlite_export_service_internal::BoundParam{ + .index = sqlite_export_service_internal::kCityLatitudeBindIndex, + .value = city.latitude, + .action = "Failed to bind SQLite city latitude"}); sqlite_export_service_internal::Bind( insert_city_stmt_, sqlite_export_service_internal::BoundParam{ @@ -165,6 +177,20 @@ uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) { kBreweryAddressPostalCodeBindIndex, .value = brewery.address.postal_code, .action = "Failed to bind SQLite brewery address postal code"}); + sqlite_export_service_internal::Bind( + insert_brewery_address_stmt_, + sqlite_export_service_internal::BoundParam{ + .index = sqlite_export_service_internal:: + kBreweryAddressLongitudeBindIndex, + .value = brewery.address.longitude, + .action = "Failed to bind SQLite brewery address longitude"}); + sqlite_export_service_internal::Bind( + insert_brewery_address_stmt_, + sqlite_export_service_internal::BoundParam{ + .index = sqlite_export_service_internal:: + kBreweryAddressLatitudeBindIndex, + .value = brewery.address.latitude, + .action = "Failed to bind SQLite brewery address latitude"}); sqlite_export_service_internal::StepStatement( db_handle_, insert_brewery_address_stmt_, diff --git a/tooling/pipeline/src/web_client/http_web_client.cc b/tooling/pipeline/src/web_client/http_web_client.cc index c9f0891e..da527d83 100644 --- a/tooling/pipeline/src/web_client/http_web_client.cc +++ b/tooling/pipeline/src/web_client/http_web_client.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include "services/logging/logger.h" @@ -67,6 +68,44 @@ std::string HttpWebClient::Get(const std::string& url) { return result->body; } +std::string HttpWebClient::Post( + const std::string& url, const std::string& body, + const std::vector>& headers) { + const auto [origin, path] = SplitUrl(url); + + httplib::Client client(origin); + client.set_follow_location(true); + client.set_connection_timeout(kConnectionTimeoutSeconds); + client.set_read_timeout(kReadTimeoutSeconds); + + httplib::Headers httplib_headers; + for (const auto& [name, value] : headers) { + httplib_headers.emplace(name, value); + } + + const httplib::Result result = + client.Post(path, httplib_headers, body, "application/json"); + + if (!result) { + throw std::runtime_error( + std::format("[HttpWebClient] Request failed for URL: {} — {}", url, + httplib::to_string(result.error()))); + } + + if (result->status < kSuccessMin || result->status >= kSuccessMax) { + if (logger_) { + logger_->Log({.level = LogLevel::Error, + .phase = PipelinePhase::Enrichment, + .message = std::format( + "[HttpWebClient] Request failed for URL: {}", url)}); + } + throw std::runtime_error(std::format( + "[HttpWebClient] HTTP {} for URL: {}", result->status, url)); + } + + return result->body; +} + std::string HttpWebClient::EncodeURL(const std::string& value) { return httplib::encode_uri_component(value); } diff --git a/web/backend/Database/Database.Seed/Database.Seed.csproj b/web/backend/Database/Database.Seed/Database.Seed.csproj index a043b08a..ee222915 100644 --- a/web/backend/Database/Database.Seed/Database.Seed.csproj +++ b/web/backend/Database/Database.Seed/Database.Seed.csproj @@ -22,6 +22,6 @@ - + diff --git a/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs b/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs index 6fcfcac5..535d1a0a 100644 --- a/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs +++ b/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs @@ -12,6 +12,6 @@ public class ConnectionStrings public static string GetSqlServerConnectionString(IConfiguration configuration) => SqlConnectionStringHelper.BuildConnectionString(configuration); - public static string SqliteConnectionString => - "Data Source=SeedData/biergarten_seed_2026-07-20T05-46-02.993511Z.sqlite"; + public static string SqliteConnectionString => "Data Source=SeedData/open-ai-test2.sqlite"; + // "Data Source=SeedData/biergarten_seed_2026-07-20T05-46-02.993511Z.sqlite"; } diff --git a/web/backend/Database/Database.Seed/PipelineData/BreweryAddress.cs b/web/backend/Database/Database.Seed/PipelineData/BreweryAddress.cs index b6902bb1..7d373338 100644 --- a/web/backend/Database/Database.Seed/PipelineData/BreweryAddress.cs +++ b/web/backend/Database/Database.Seed/PipelineData/BreweryAddress.cs @@ -5,4 +5,8 @@ public sealed record BreweryAddress public required City City { get; init; } public required string PostalCode { get; init; } + + public required double Longitude { get; init; } + + public required double Latitude { get; init; } } diff --git a/web/backend/Database/Database.Seed/SeedData/open-ai-test.sqlite b/web/backend/Database/Database.Seed/SeedData/open-ai-test.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..e3ead9ab019b0fd9e3aeb91782cabe30adc981ee GIT binary patch literal 94208 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 z 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 records = []; while (await reader.ReadAsync(cancellationToken)) @@ -85,6 +87,8 @@ public sealed class PipelineSeedDataReader { City = cities[reader.GetInt64(cityIdIndex)], PostalCode = reader.GetString(postalCodeIndex), + Longitude = reader.GetDouble(longitudeIndex), + Latitude = reader.GetDouble(latitudeIndex), }, } ); diff --git a/web/backend/Domain/Domain.Entities/Entities/BreweryPost.cs b/web/backend/Domain/Domain.Entities/Entities/BreweryPost.cs index 4812ffa1..46a4efca 100644 --- a/web/backend/Domain/Domain.Entities/Entities/BreweryPost.cs +++ b/web/backend/Domain/Domain.Entities/Entities/BreweryPost.cs @@ -7,6 +7,7 @@ public sealed class BreweryPost // FK References public Guid PostedById { get; set; } + public string PostedBy { get; set; } = string.Empty; // Attributes public string BreweryName { get; set; } = string.Empty; diff --git a/web/generate-env.sh b/web/generate-env.sh new file mode 100755 index 00000000..89200b0e --- /dev/null +++ b/web/generate-env.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# +# generate_env.sh +# +# Generates a Biergarten App environment file with freshly randomized +# secrets (DB password, JWT signing secrets, SMTP credentials), based on +# the .env.example template. Non-secret configuration values are left +# untouched so the file still matches the documented template structure. +# +# Usage: +# ./generate_env.sh [output_file] [template_file] +# +# output_file Path to write the generated env file to (default: .env) +# template_file Path to the source template (default: .env.example) + +#!/usr/bin/env bash + +set -euo pipefail + +OUTPUT_FILE="${1:-.env}" +TEMPLATE_FILE="${2:-.env.example}" + +if ! command -v openssl >/dev/null 2>&1; then + echo "Error: openssl is required but was not found on PATH." >&2 + exit 1 +fi + +if [[ ! -f "$TEMPLATE_FILE" ]]; then + echo "Error: template file '$TEMPLATE_FILE' not found." >&2 + echo "Pass the template path explicitly: ./generate_env.sh