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:
@@ -1,24 +1,121 @@
|
||||
/**
|
||||
* @file data_generation/llama/generate_user.cc
|
||||
* @brief Generates locale-aware user profiles with strict two-line formatting,
|
||||
* retry handling, and output sanitization for downstream parsing.
|
||||
* @brief Builds persona/name-grounded user prompts, performs retry-based
|
||||
* inference, and validates structured JSON output for user records.
|
||||
*/
|
||||
|
||||
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "data_generation/llama_generator.h"
|
||||
#include "data_generation/llama_generator_helpers.h"
|
||||
|
||||
// TODO: Implement locale-aware user profile generation.
|
||||
// Current implementation returns a hardcoded test value and ignores the
|
||||
// locale parameter. Future implementation should:
|
||||
// 1. Load a USER_GENERATION.md prompt template with locale context
|
||||
// 2. Perform LLM inference with locale-specific username/bio generation
|
||||
// 3. Parse and validate JSON output with retry handling (similar to brewery)
|
||||
// 4. Return locale-aware username and biography
|
||||
UserResult LlamaGenerator::GenerateUser(const std::string& locale) {
|
||||
return {.username = "test_user",
|
||||
.bio = std::format("This is a test user profile from {}.", locale)};
|
||||
// GBNF grammar for structured user JSON output.
|
||||
static constexpr std::string_view kUserJsonGrammar = R"json_user(
|
||||
root ::= thought-block "{" ws "\"username\"" ws ":" ws string ws "," ws "\"bio\"" ws ":" ws string ws "," ws "\"activity_weight\"" ws ":" ws number ws "}" ws
|
||||
thought-block ::= [^{]*
|
||||
ws ::= [ \t\n\r]*
|
||||
string ::= "\"" char+ "\""
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] escape
|
||||
escape ::= ["\\/bfnrt] | "u" hex hex hex hex
|
||||
hex ::= [0-9a-fA-F]
|
||||
number ::= "-"? ("0" | [1-9] [0-9]*) ("." [0-9]+)?
|
||||
)json_user";
|
||||
|
||||
static constexpr int kUserInitialMaxTokens = 1200;
|
||||
|
||||
UserResult LlamaGenerator::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## GENDER:\n{}\n\n## CITY:\n{}\n\n## COUNTRY:\n{}\n\n"
|
||||
"## PERSONA:\n{}\n\n## PERSONA DESCRIPTION:\n{}\n\n## STYLE "
|
||||
"AFFINITIES:\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);
|
||||
|
||||
constexpr int max_attempts = 3;
|
||||
std::string raw;
|
||||
std::string last_error;
|
||||
int max_tokens = kUserInitialMaxTokens;
|
||||
|
||||
for (int attempt = 0; attempt < max_attempts; ++attempt) {
|
||||
raw = this->Infer(system_prompt, user_prompt, max_tokens,
|
||||
kUserJsonGrammar);
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Debug,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("LlamaGenerator: raw output (attempt {}): {}",
|
||||
attempt + 1, raw)});
|
||||
}
|
||||
|
||||
UserResult user;
|
||||
const std::optional<std::string> validation_error =
|
||||
ValidateUserJson(raw, user);
|
||||
|
||||
if (!validation_error.has_value()) {
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Info,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("LlamaGenerator: 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("LlamaGenerator: malformed user JSON (attempt {}): {}",
|
||||
attempt + 1, *validation_error)});
|
||||
}
|
||||
|
||||
user_prompt = std::format(
|
||||
"Your previous response was invalid. Error: {}\nReturn the thought "
|
||||
"process before the JSON if needed, then return ONLY valid JSON with "
|
||||
"exactly these keys, in this exact order: {{\"username\": \"<handle "
|
||||
"derived from the given name>\", \"bio\": \"<first-person bio "
|
||||
"grounded in the persona>\", \"activity_weight\": <number between 0 "
|
||||
"and 1>}}.\nDo not include markdown, comments, extra keys, or "
|
||||
"literal placeholder values.\n\n{}",
|
||||
*validation_error, retry_context);
|
||||
}
|
||||
|
||||
if (logger_) {
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Error,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format(
|
||||
"LlamaGenerator: malformed user response after {} attempts: {}",
|
||||
max_attempts, last_error.empty() ? raw : last_error)});
|
||||
}
|
||||
throw std::runtime_error("LlamaGenerator: malformed user response");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user