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
This commit is contained in:
2026-08-23 15:49:28 -04:00
committed by Aaron Po
parent 31da1d557c
commit e738fe484a
35 changed files with 1327 additions and 343 deletions

View File

@@ -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 <cstddef>
#include <optional>
#include <string>
#include <string_view>
#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<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_GENERATION_JSON_VALIDATION_H_

View File

@@ -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 <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#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<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_

View File

@@ -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 <memory>
#include <string>
#include <string_view>
#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<ILogger> logger,
std::unique_ptr<IPromptDirectory> prompt_directory,
std::unique_ptr<WebClient> 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<ILogger> logger_;
std::unique_ptr<IPromptDirectory> prompt_directory_;
std::unique_ptr<WebClient> web_client_;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_OPENAI_GENERATOR_H_

View File

@@ -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 <string_view>
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_