mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-06-01 10:04:00 +00:00
Compare commits
9 Commits
main-2.0
...
d40ce34363
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d40ce34363 | ||
|
|
a8e0ced8ba | ||
|
|
bbe8970bf6 | ||
|
|
6657015ee3 | ||
|
|
915301fccb | ||
|
|
d9412df86c | ||
|
|
660e34483e | ||
|
|
aa46cf2b4b | ||
|
|
fd6ba35f68 |
13
.config/dotnet-tools.json
Normal file
13
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"csharpier": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"commands": [
|
||||||
|
"csharpier"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/csharpier.json",
|
||||||
|
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"indentSize": 4,
|
"indentSize": 4,
|
||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
|||||||
archive/** linguist-vendored
|
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,7 +7,6 @@ assignees: []
|
|||||||
---
|
---
|
||||||
|
|
||||||
## User Story
|
## User Story
|
||||||
|
|
||||||
**As a** (who wants to accomplish something)
|
**As a** (who wants to accomplish something)
|
||||||
**I want to** (what they want to accomplish)
|
**I want to** (what they want to accomplish)
|
||||||
**So that** (why they want to accomplish that thing)
|
**So that** (why they want to accomplish that thing)
|
||||||
@@ -16,18 +15,29 @@ assignees: []
|
|||||||
|
|
||||||
### Scenario 1
|
### Scenario 1
|
||||||
|
|
||||||
Given ... When ... Then ...
|
|
||||||
|
Given ...
|
||||||
|
When ...
|
||||||
|
Then ...
|
||||||
|
|
||||||
|
|
||||||
### Scenario 2
|
### Scenario 2
|
||||||
|
|
||||||
Given ... When ... Then ...
|
|
||||||
|
Given ...
|
||||||
|
When ...
|
||||||
|
Then ...
|
||||||
|
|
||||||
|
|
||||||
### Scenario 3
|
### Scenario 3
|
||||||
|
|
||||||
Given ... When ... Then ...
|
|
||||||
|
Given ...
|
||||||
|
When ...
|
||||||
|
Then ...
|
||||||
|
|
||||||
|
|
||||||
## Subtasks
|
## Subtasks
|
||||||
|
|
||||||
- [ ] Task 1
|
- [ ] Task 1
|
||||||
- [ ] Task 2
|
- [ ] Task 2
|
||||||
- [ ] Task 3
|
- [ ] Task 3
|
||||||
962
LICENSE.md
962
LICENSE.md
File diff suppressed because it is too large
Load Diff
161
README.md
161
README.md
@@ -1,56 +1,40 @@
|
|||||||
# The Biergarten App
|
# The Biergarten App
|
||||||
|
|
||||||
The Biergarten App is a full-stack directory and discovery platform for
|
The Biergarten App is a multi-project monorepo with a .NET backend and an active React
|
||||||
breweries. It features a robust user authentication system, a searchable
|
Router frontend in `src/Website`. The current website focuses on account flows, theme
|
||||||
database of brewery locations, and a custom offline data-generation pipeline
|
switching, shared UI components, Storybook coverage, and integration with the API.
|
||||||
that uses LLMs (Llama.cpp) and Wikipedia to synthesize realistic seed data.
|
|
||||||
|
|
||||||
It features:
|
## Documentation
|
||||||
|
|
||||||
- A .NET backend (Web API + database migrations/seed) under `web/backend/`
|
- [Getting Started](docs/getting-started.md) - Local setup for backend and active website
|
||||||
- A server-rendered React website (React Router + Vite) under `web/frontend/`
|
- [Architecture](docs/architecture.md) - Current backend and frontend architecture
|
||||||
- A C++20 “pipeline” CLI for generating seed data under `tooling/pipeline/`
|
- [Docker Guide](docs/docker.md) - Container-based backend development and testing
|
||||||
|
- [Testing](docs/testing.md) - Backend and frontend test commands
|
||||||
Specialized documentation (setup, architecture, docker, testing, diagrams, and
|
- [Environment Variables](docs/environment-variables.md) - Active configuration reference
|
||||||
pipeline notes) lives under `docs/`.
|
- [Token Validation](docs/token-validation.md) - JWT validation architecture
|
||||||
|
- [Legacy Website Archive](docs/archive/legacy-website-v1.md) - Archived notes for the old Next.js frontend
|
||||||
## Documentation (Start Here)
|
|
||||||
|
|
||||||
Website + backend (active stack):
|
|
||||||
|
|
||||||
- [Getting Started](docs/website/getting-started.md)
|
|
||||||
- [Architecture](docs/architecture.md)
|
|
||||||
- [Docker Guide](docs/website/docker.md)
|
|
||||||
- [Testing](docs/website/testing.md)
|
|
||||||
- [Environment Variables](docs/website/environment-variables.md)
|
|
||||||
- [Token Validation](docs/website/token-validation.md)
|
|
||||||
|
|
||||||
Data generation pipeline (C++):
|
|
||||||
|
|
||||||
- [Pipeline README](docs/pipeline/README.md)
|
|
||||||
- [Ethics & Known Issues](docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md)
|
|
||||||
|
|
||||||
## Diagrams
|
## Diagrams
|
||||||
|
|
||||||
- [Architecture](docs/website/diagrams-out/architecture.svg)
|
- [Architecture](docs/diagrams-out/architecture.svg) - Layered architecture
|
||||||
- [Deployment](docs/website/diagrams-out/deployment.svg)
|
- [Deployment](docs/diagrams-out/deployment.svg) - Docker topology
|
||||||
- [Authentication Flow](docs/website/diagrams-out/authentication-flow.svg)
|
- [Authentication Flow](docs/diagrams-out/authentication-flow.svg) - Auth sequence
|
||||||
- [Database Schema](docs/website/diagrams-out/database-schema.svg)
|
- [Database Schema](docs/diagrams-out/database-schema.svg) - Entity relationships
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
Active areas in the repository:
|
Active areas in the repository:
|
||||||
|
|
||||||
- .NET 10 backend (layered architecture) + SQL Server
|
- .NET 10 backend with layered architecture and SQL Server
|
||||||
- React 19 website (React Router 7 + Vite)
|
- React Router 7 website in `src/Website`
|
||||||
- Shared Biergarten theme system + Storybook coverage
|
- Shared Biergarten theme system with a theme guide route
|
||||||
- Auth flows and account/email integration (local Mailpit in dev compose)
|
- Storybook stories and browser-based checks for shared UI
|
||||||
- Data generation pipeline with C++ and Llama.cpp
|
- Auth demo flows for home, login, register, dashboard, logout, and confirmation
|
||||||
|
- Toast-based feedback for auth outcomes
|
||||||
|
|
||||||
Archived/reference areas:
|
Legacy area retained for reference:
|
||||||
|
|
||||||
- `archive/next-js-web-app/` contains an older Next.js frontend retained for
|
- `src/Website-v1` contains the archived Next.js frontend and is no longer the active website
|
||||||
reference
|
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
@@ -59,43 +43,36 @@ Archived/reference areas:
|
|||||||
- **UI Documentation**: Storybook 10, Vitest browser mode, Playwright
|
- **UI Documentation**: Storybook 10, Vitest browser mode, Playwright
|
||||||
- **Testing**: xUnit, Reqnroll (BDD), FluentAssertions, Moq
|
- **Testing**: xUnit, Reqnroll (BDD), FluentAssertions, Moq
|
||||||
- **Infrastructure**: Docker, Docker Compose
|
- **Infrastructure**: Docker, Docker Compose
|
||||||
- **Security**: Argon2id password hashing, JWT access/refresh/confirmation
|
- **Security**: Argon2id password hashing, JWT access/refresh/confirmation tokens
|
||||||
tokens
|
|
||||||
- **Data Pipeline**: C++20, CMake, Boost, libcurl, SQLite, llama.cpp
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
For full setup details, use [Getting Started](docs/website/getting-started.md).
|
### Backend
|
||||||
This section is the shortest path to a working dev environment.
|
|
||||||
|
|
||||||
### Backend (Docker)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/aaronpo97/the-biergarten-app
|
git clone https://github.com/aaronpo97/the-biergarten-app
|
||||||
cd the-biergarten-app
|
cd the-biergarten-app
|
||||||
|
cp .env.example .env.dev
|
||||||
cp web/.env.example web/.env.dev
|
docker compose -f docker-compose.dev.yaml up -d
|
||||||
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up --build -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Backend access:
|
Backend access:
|
||||||
|
|
||||||
- API Swagger: http://localhost:8080/swagger
|
- API Swagger: http://localhost:8080/swagger
|
||||||
- Health Check: http://localhost:8080/health
|
- Health Check: http://localhost:8080/health
|
||||||
- Mailpit UI (dev SMTP): http://localhost:8025
|
|
||||||
|
|
||||||
### Frontend (Node)
|
### Frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd web/frontend
|
cd src/Website
|
||||||
npm install
|
npm install
|
||||||
API_BASE_URL=http://localhost:8080 SESSION_SECRET=dev-secret-change-me npm run dev
|
API_BASE_URL=http://localhost:8080 SESSION_SECRET=dev-secret npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional frontend tools:
|
Optional frontend tools:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd web/frontend
|
cd src/Website
|
||||||
npm run storybook
|
npm run storybook
|
||||||
npm run test:storybook
|
npm run test:storybook
|
||||||
npm run test:storybook:playwright
|
npm run test:storybook:playwright
|
||||||
@@ -104,42 +81,62 @@ npm run test:storybook:playwright
|
|||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
web/
|
src/Core/ Backend projects (.NET)
|
||||||
backend/ .NET API + domain/service/infrastructure + DB projects
|
src/Website/ Active React Router frontend
|
||||||
frontend/ React Router website + Storybook + Playwright/Vitest
|
src/Website-v1/ Archived legacy Next.js frontend
|
||||||
|
docs/ Active project documentation
|
||||||
tooling/
|
docs/archive/ Archived legacy documentation
|
||||||
pipeline/ C++20 seed-data generation CLI (CMake)
|
|
||||||
|
|
||||||
docs/
|
|
||||||
architecture.md High-level architecture overview
|
|
||||||
website/ Backend/frontend setup, docker, testing, diagrams
|
|
||||||
pipeline/ Pipeline docs, ethics notes, PlantUML diagrams
|
|
||||||
|
|
||||||
archive/
|
|
||||||
next-js-web-app/ Older Next.js frontend (reference only)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
Implemented today:
|
||||||
|
|
||||||
|
- User registration and login against the API
|
||||||
|
- JWT-based auth with access, refresh, and confirmation flows
|
||||||
|
- SQL Server migrations and seed projects
|
||||||
|
- Shared form components and auth screens
|
||||||
|
- Theme switching with Lager, Stout, Cassis, and Weizen variants
|
||||||
|
- Storybook documentation and automated story interaction tests
|
||||||
|
- Toast feedback for auth-related outcomes
|
||||||
|
|
||||||
|
Planned next:
|
||||||
|
|
||||||
|
- Brewery discovery and management
|
||||||
|
- Beer reviews and ratings
|
||||||
|
- Social follow relationships
|
||||||
|
- Geospatial brewery experiences
|
||||||
|
- Additional frontend routes beyond the auth demo
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run the backend test stack with Docker:
|
Backend suites:
|
||||||
|
|
||||||
|
- `API.Specs` - integration tests
|
||||||
|
- `Infrastructure.Repository.Tests` - repository unit tests
|
||||||
|
- `Service.Auth.Tests` - service unit tests
|
||||||
|
|
||||||
|
Frontend suites:
|
||||||
|
|
||||||
|
- Storybook interaction tests via Vitest
|
||||||
|
- Storybook browser regression checks via Playwright
|
||||||
|
|
||||||
|
Run all backend tests with Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit
|
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Testing](docs/website/testing.md) for the full command list.
|
See [Testing](docs/testing.md) for the full command list.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Common active variables:
|
Common active variables:
|
||||||
|
|
||||||
- Backend/Docker: `DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`,
|
- Backend: `DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`
|
||||||
`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`,
|
- Frontend: `API_BASE_URL`, `SESSION_SECRET`, `NODE_ENV`
|
||||||
`WEBSITE_BASE_URL`
|
|
||||||
- Frontend runtime: `API_BASE_URL`, `SESSION_SECRET`, `NODE_ENV`
|
|
||||||
|
|
||||||
See [Environment Variables](docs/website/environment-variables.md) for details.
|
See [Environment Variables](docs/environment-variables.md) for details.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -148,3 +145,15 @@ See [Environment Variables](docs/website/environment-variables.md) for details.
|
|||||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
5. Open a Pull Request
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
1. Start development environment: `docker compose -f docker-compose.dev.yaml up -d`
|
||||||
|
2. Make changes to code
|
||||||
|
3. Run tests: `docker compose -f docker-compose.test.yaml up --abort-on-container-exit`
|
||||||
|
4. Rebuild if needed: `docker compose -f docker-compose.dev.yaml up -d --build api.core`
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Documentation**: [docs/](docs/)
|
||||||
|
- **Architecture**: See [Architecture Guide](docs/architecture.md)
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-dev:/var/opt/mssql
|
- sqlserverdata-dev:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1",
|
|
||||||
]
|
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
@@ -32,7 +28,7 @@ services:
|
|||||||
sqlserver:
|
sqlserver:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: ./backend/Database
|
context: ./src/Core/Database
|
||||||
dockerfile: Database.Migrations/Dockerfile
|
dockerfile: Database.Migrations/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -56,7 +52,7 @@ services:
|
|||||||
database.migrations:
|
database.migrations:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: Database/Database.Seed/Dockerfile
|
dockerfile: Database/Database.Seed/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -13,11 +13,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-dev:/var/opt/mssql
|
- sqlserverdata-dev:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: [ "CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1" ]
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1",
|
|
||||||
]
|
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
@@ -32,7 +28,7 @@ services:
|
|||||||
sqlserver:
|
sqlserver:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: ./backend/Database
|
context: ./src/Core/Database
|
||||||
dockerfile: Database.Migrations/Dockerfile
|
dockerfile: Database.Migrations/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -56,7 +52,7 @@ services:
|
|||||||
database.migrations:
|
database.migrations:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: Database/Database.Seed/Dockerfile
|
dockerfile: Database/Database.Seed/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -79,7 +75,7 @@ services:
|
|||||||
database.seed:
|
database.seed:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: API/API.Core/Dockerfile
|
dockerfile: API/API.Core/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -13,11 +13,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-dev:/var/opt/mssql
|
- sqlserverdata-dev:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: [ "CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1" ]
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1",
|
|
||||||
]
|
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
@@ -32,7 +28,7 @@ services:
|
|||||||
sqlserver:
|
sqlserver:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: ./backend/Database
|
context: ./src/Core/Database
|
||||||
dockerfile: Database.Migrations/Dockerfile
|
dockerfile: Database.Migrations/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -70,7 +66,7 @@ services:
|
|||||||
database.migrations:
|
database.migrations:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: Database/Database.Seed/Dockerfile
|
dockerfile: Database/Database.Seed/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -11,11 +11,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-prod:/var/opt/mssql
|
- sqlserverdata-prod:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1",
|
|
||||||
]
|
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
@@ -31,7 +27,7 @@ services:
|
|||||||
sqlserver:
|
sqlserver:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: ./backend/Database
|
context: ./src/Core/Database
|
||||||
dockerfile: Database.Migrations/Dockerfile
|
dockerfile: Database.Migrations/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -54,7 +50,7 @@ services:
|
|||||||
sqlserver:
|
sqlserver:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: API/API.Core/Dockerfile
|
dockerfile: API/API.Core/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -12,11 +12,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-test:/var/opt/mssql
|
- sqlserverdata-test:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: [ "CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1" ]
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1",
|
|
||||||
]
|
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
@@ -32,7 +28,7 @@ services:
|
|||||||
sqlserver:
|
sqlserver:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: ./backend/Database
|
context: ./src/Core/Database
|
||||||
dockerfile: Database.Migrations/Dockerfile
|
dockerfile: Database.Migrations/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -56,7 +52,7 @@ services:
|
|||||||
database.migrations:
|
database.migrations:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: Database/Database.Seed/Dockerfile
|
dockerfile: Database/Database.Seed/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -79,7 +75,7 @@ services:
|
|||||||
database.seed:
|
database.seed:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: API/API.Specs/Dockerfile
|
dockerfile: API/API.Specs/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -107,7 +103,7 @@ services:
|
|||||||
database.seed:
|
database.seed:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: Infrastructure/Infrastructure.Repository.Tests/Dockerfile
|
dockerfile: Infrastructure/Infrastructure.Repository.Tests/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -127,7 +123,7 @@ services:
|
|||||||
database.seed:
|
database.seed:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./src/Core
|
||||||
dockerfile: Service/Service.Auth.Tests/Dockerfile
|
dockerfile: Service/Service.Auth.Tests/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
@@ -4,28 +4,24 @@ This document describes the active architecture of The Biergarten App.
|
|||||||
|
|
||||||
## High-Level Overview
|
## High-Level Overview
|
||||||
|
|
||||||
The Biergarten App is a monorepo with a clear split between the backend and the
|
The Biergarten App is a monorepo with a clear split between the backend and the active
|
||||||
active website:
|
website:
|
||||||
|
|
||||||
- **Backend**: .NET 10 Web API with SQL Server and a layered architecture
|
- **Backend**: .NET 10 Web API with SQL Server and a layered architecture
|
||||||
- **Frontend**: React 19 + React Router 7 website in `src/Website`
|
- **Frontend**: React 19 + React Router 7 website in `src/Website`
|
||||||
- **Architecture Style**: Layered backend plus server-rendered React frontend
|
- **Architecture Style**: Layered backend plus server-rendered React frontend
|
||||||
|
|
||||||
The legacy Next.js frontend has been retained in `src/Website-v1` for reference
|
The legacy Next.js frontend has been retained in `src/Website-v1` for reference only and is
|
||||||
only and is documented in
|
documented in [archive/legacy-website-v1.md](archive/legacy-website-v1.md).
|
||||||
[archive/legacy-website-v1.md](archive/legacy-website-v1.md).
|
|
||||||
|
|
||||||
## Diagrams
|
## Diagrams
|
||||||
|
|
||||||
For visual representations, see:
|
For visual representations, see:
|
||||||
|
|
||||||
- [architecture.svg](diagrams-out/architecture.svg) - Layered architecture
|
- [architecture.svg](diagrams-out/architecture.svg) - Layered architecture diagram
|
||||||
diagram
|
|
||||||
- [deployment.svg](diagrams-out/deployment.svg) - Docker deployment diagram
|
- [deployment.svg](diagrams-out/deployment.svg) - Docker deployment diagram
|
||||||
- [authentication-flow.svg](diagrams-out/authentication-flow.svg) -
|
- [authentication-flow.svg](diagrams-out/authentication-flow.svg) - Authentication workflow
|
||||||
Authentication workflow
|
- [database-schema.svg](diagrams-out/database-schema.svg) - Database relationships
|
||||||
- [database-schema.svg](diagrams-out/database-schema.svg) - Database
|
|
||||||
relationships
|
|
||||||
|
|
||||||
## Backend Architecture
|
## Backend Architecture
|
||||||
|
|
||||||
@@ -222,8 +218,7 @@ public interface IAuthRepository
|
|||||||
|
|
||||||
### Active Website (`src/Website`)
|
### Active Website (`src/Website`)
|
||||||
|
|
||||||
The current website is a React Router 7 application with server-side rendering
|
The current website is a React Router 7 application with server-side rendering enabled.
|
||||||
enabled.
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/Website/
|
src/Website/
|
||||||
@@ -249,22 +244,20 @@ src/Website/
|
|||||||
|
|
||||||
### Theme System
|
### Theme System
|
||||||
|
|
||||||
The active website uses semantic DaisyUI theme tokens backed by four Biergarten
|
The active website uses semantic DaisyUI theme tokens backed by four Biergarten themes:
|
||||||
themes:
|
|
||||||
|
|
||||||
- Biergarten Lager
|
- Biergarten Lager
|
||||||
- Biergarten Stout
|
- Biergarten Stout
|
||||||
- Biergarten Cassis
|
- Biergarten Cassis
|
||||||
- Biergarten Weizen
|
- Biergarten Weizen
|
||||||
|
|
||||||
All component styling should prefer semantic tokens such as `primary`,
|
All component styling should prefer semantic tokens such as `primary`, `success`,
|
||||||
`success`, `surface`, and `highlight` instead of hard-coded color values.
|
`surface`, and `highlight` instead of hard-coded color values.
|
||||||
|
|
||||||
### Legacy Frontend
|
### Legacy Frontend
|
||||||
|
|
||||||
The previous Next.js frontend has been archived at `src/Website-v1`. Active
|
The previous Next.js frontend has been archived at `src/Website-v1`. Active product and
|
||||||
product and engineering documentation should point to `src/Website`, while
|
engineering documentation should point to `src/Website`, while legacy notes live in
|
||||||
legacy notes live in
|
|
||||||
[archive/legacy-website-v1.md](archive/legacy-website-v1.md).
|
[archive/legacy-website-v1.md](archive/legacy-website-v1.md).
|
||||||
|
|
||||||
## Security Architecture
|
## Security Architecture
|
||||||
@@ -394,8 +387,8 @@ For details, see [Docker Guide](docker.md).
|
|||||||
|
|
||||||
### Health Checks
|
### Health Checks
|
||||||
|
|
||||||
**SQL Server**: Validates database connectivity **API**: Checks service health
|
**SQL Server**: Validates database connectivity **API**: Checks service health and
|
||||||
and dependencies
|
dependencies
|
||||||
|
|
||||||
**Configuration**:
|
**Configuration**:
|
||||||
|
|
||||||
|
|||||||
56
docs/archive/legacy-website-v1.md
Normal file
56
docs/archive/legacy-website-v1.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Legacy Website Archive (`src/Website-v1`)
|
||||||
|
|
||||||
|
This archive captures high-level notes about the previous Biergarten frontend so active
|
||||||
|
project documentation can focus on the current website in `src/Website`.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- `src/Website-v1` is retained for historical reference only
|
||||||
|
- It is not the active frontend used by current setup, docs, or testing guidance
|
||||||
|
- New product and engineering work should target `src/Website`
|
||||||
|
|
||||||
|
## Legacy Stack Summary
|
||||||
|
|
||||||
|
The archived frontend used a different application model from the current website:
|
||||||
|
|
||||||
|
- Next.js 14
|
||||||
|
- React 18
|
||||||
|
- Prisma
|
||||||
|
- Postgres / Neon-hosted database workflows
|
||||||
|
- Next.js API routes and server-side controllers
|
||||||
|
- Additional third-party integrations such as Cloudinary, Mapbox, and SparkPost
|
||||||
|
|
||||||
|
## Why It Was Archived
|
||||||
|
|
||||||
|
The active website moved to a React Router-based frontend that talks directly to the .NET
|
||||||
|
API. As part of that shift, the main docs were updated to describe:
|
||||||
|
|
||||||
|
- `src/Website` as the active frontend
|
||||||
|
- React Router route modules and server rendering
|
||||||
|
- Storybook-based component documentation and tests
|
||||||
|
- Current frontend runtime variables: `API_BASE_URL`, `SESSION_SECRET`, and `NODE_ENV`
|
||||||
|
|
||||||
|
## Legacy Documentation Topics Moved Out of Active Docs
|
||||||
|
|
||||||
|
The following categories were removed from active documentation and intentionally archived:
|
||||||
|
|
||||||
|
- Next.js application structure guidance
|
||||||
|
- Prisma and Postgres frontend setup
|
||||||
|
- Legacy frontend environment variables
|
||||||
|
- External service setup that only applied to `src/Website-v1`
|
||||||
|
- Old frontend local setup instructions
|
||||||
|
|
||||||
|
## When To Use This Archive
|
||||||
|
|
||||||
|
Use this file only if you need to:
|
||||||
|
|
||||||
|
- inspect the historical frontend implementation
|
||||||
|
- compare old flows against the current website
|
||||||
|
- migrate or recover legacy logic from `src/Website-v1`
|
||||||
|
|
||||||
|
For all active work, use:
|
||||||
|
|
||||||
|
- [Getting Started](../getting-started.md)
|
||||||
|
- [Architecture](../architecture.md)
|
||||||
|
- [Environment Variables](../environment-variables.md)
|
||||||
|
- [Testing](../testing.md)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Docker Guide
|
# Docker Guide
|
||||||
|
|
||||||
This document covers Docker deployment, configuration, and troubleshooting for
|
This document covers Docker deployment, configuration, and troubleshooting for The
|
||||||
The Biergarten App.
|
Biergarten App.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -13,8 +13,7 @@ The project uses Docker Compose to orchestrate multiple services:
|
|||||||
- .NET API
|
- .NET API
|
||||||
- Test runners
|
- Test runners
|
||||||
|
|
||||||
See the [deployment diagram](diagrams/pdf/deployment.pdf) for visual
|
See the [deployment diagram](diagrams/pdf/deployment.pdf) for visual representation.
|
||||||
representation.
|
|
||||||
|
|
||||||
## Docker Compose Environments
|
## Docker Compose Environments
|
||||||
|
|
||||||
@@ -145,11 +144,7 @@ api.core / tests (start when ready)
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ['CMD-SHELL', "sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1'"]
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1'",
|
|
||||||
]
|
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
@@ -214,16 +209,16 @@ Each environment uses isolated bridge networks:
|
|||||||
All containers are configured via environment variables from `.env` files:
|
All containers are configured via environment variables from `.env` files:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
env_file: ".env.dev" # or .env.test, .env.prod
|
env_file: '.env.dev' # or .env.test, .env.prod
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
ASPNETCORE_ENVIRONMENT: "Development"
|
ASPNETCORE_ENVIRONMENT: 'Development'
|
||||||
DOTNET_RUNNING_IN_CONTAINER: "true"
|
DOTNET_RUNNING_IN_CONTAINER: 'true'
|
||||||
DB_SERVER: "${DB_SERVER}"
|
DB_SERVER: '${DB_SERVER}'
|
||||||
DB_NAME: "${DB_NAME}"
|
DB_NAME: '${DB_NAME}'
|
||||||
DB_USER: "${DB_USER}"
|
DB_USER: '${DB_USER}'
|
||||||
DB_PASSWORD: "${DB_PASSWORD}"
|
DB_PASSWORD: '${DB_PASSWORD}'
|
||||||
JWT_SECRET: "${JWT_SECRET}"
|
JWT_SECRET: '${JWT_SECRET}'
|
||||||
```
|
```
|
||||||
|
|
||||||
For complete list, see [Environment Variables](environment-variables.md).
|
For complete list, see [Environment Variables](environment-variables.md).
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Environment Variables
|
# Environment Variables
|
||||||
|
|
||||||
This document covers the active environment variables used by the current
|
This document covers the active environment variables used by the current Biergarten
|
||||||
Biergarten stack.
|
stack.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ Direct environment variable access via `Environment.GetEnvironmentVariable()`.
|
|||||||
|
|
||||||
### Frontend (`src/Website`)
|
### Frontend (`src/Website`)
|
||||||
|
|
||||||
The active website reads runtime values from the server environment for its auth
|
The active website reads runtime values from the server environment for its auth and API
|
||||||
and API integration.
|
integration.
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
@@ -54,15 +54,14 @@ Provide complete connection string:
|
|||||||
DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
|
DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Priority**: `DB_CONNECTION_STRING` is checked first. If not found, connection
|
**Priority**: `DB_CONNECTION_STRING` is checked first. If not found, connection string is
|
||||||
string is built from components.
|
built from components.
|
||||||
|
|
||||||
**Implementation**: See `DefaultSqlConnectionFactory.cs`
|
**Implementation**: See `DefaultSqlConnectionFactory.cs`
|
||||||
|
|
||||||
### JWT Authentication Secrets (Backend)
|
### JWT Authentication Secrets (Backend)
|
||||||
|
|
||||||
The backend uses separate secrets for different token types to enable
|
The backend uses separate secrets for different token types to enable independent key rotation and validation isolation.
|
||||||
independent key rotation and validation isolation.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Access token secret (1-hour tokens)
|
# Access token secret (1-hour tokens)
|
||||||
@@ -132,8 +131,8 @@ DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution
|
|||||||
|
|
||||||
## Frontend Variables (`src/Website`)
|
## Frontend Variables (`src/Website`)
|
||||||
|
|
||||||
The active website does not use the old Next.js/Prisma environment model. Its
|
The active website does not use the old Next.js/Prisma environment model. Its core runtime
|
||||||
core runtime variables are:
|
variables are:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
API_BASE_URL=http://localhost:8080 # Base URL for the .NET API
|
API_BASE_URL=http://localhost:8080 # Base URL for the .NET API
|
||||||
@@ -209,10 +208,9 @@ cp .env.example .env.dev
|
|||||||
|
|
||||||
## Legacy Frontend Variables
|
## Legacy Frontend Variables
|
||||||
|
|
||||||
Variables for the archived Next.js frontend (`src/Website-v1`) have been removed
|
Variables for the archived Next.js frontend (`src/Website-v1`) have been removed from this
|
||||||
from this active reference. See
|
active reference. See [archive/legacy-website-v1.md](archive/legacy-website-v1.md) if you
|
||||||
[archive/legacy-website-v1.md](archive/legacy-website-v1.md) if you need the
|
need the legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
|
||||||
legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
|
|
||||||
|
|
||||||
**Docker Compose Mapping**:
|
**Docker Compose Mapping**:
|
||||||
|
|
||||||
@@ -245,8 +243,8 @@ legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
|
|||||||
| `MSSQL_PID` | | | ✓ | No | SQL Server edition |
|
| `MSSQL_PID` | | | ✓ | No | SQL Server edition |
|
||||||
| `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag |
|
| `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag |
|
||||||
|
|
||||||
\* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`,
|
\* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`, `DB_NAME`,
|
||||||
`DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided.
|
`DB_USER`, `DB_PASSWORD`) must be provided.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
@@ -260,8 +258,8 @@ Variables are validated at startup:
|
|||||||
|
|
||||||
### Frontend Validation
|
### Frontend Validation
|
||||||
|
|
||||||
The active website relies on runtime defaults for local development and the
|
The active website relies on runtime defaults for local development and the surrounding
|
||||||
surrounding server environment in deployed environments.
|
server environment in deployed environments.
|
||||||
|
|
||||||
- `API_BASE_URL` defaults to `http://localhost:8080`
|
- `API_BASE_URL` defaults to `http://localhost:8080`
|
||||||
- `SESSION_SECRET` falls back to a development-only local secret
|
- `SESSION_SECRET` falls back to a development-only local secret
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
This guide covers local setup for the current Biergarten stack: the .NET backend
|
This guide covers local setup for the current Biergarten stack: the .NET backend in
|
||||||
in `src/Core` and the active React Router frontend in `src/Website`.
|
`src/Core` and the active React Router frontend in `src/Website`.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -128,9 +128,8 @@ dotnet run --project API/API.Core/API.Core.csproj
|
|||||||
|
|
||||||
## Legacy Frontend Note
|
## Legacy Frontend Note
|
||||||
|
|
||||||
The previous Next.js frontend now lives in `src/Website-v1` and is not the
|
The previous Next.js frontend now lives in `src/Website-v1` and is not the active website.
|
||||||
active website. Legacy setup details have been moved to
|
Legacy setup details have been moved to [docs/archive/legacy-website-v1.md](archive/legacy-website-v1.md).
|
||||||
[docs/archive/legacy-website-v1.md](archive/legacy-website-v1.md).
|
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
skinparam shadowing false
|
|
||||||
skinparam backgroundColor #FCFCF7
|
|
||||||
skinparam defaultFontName "DM Sans"
|
|
||||||
skinparam defaultFontColor #14180C
|
|
||||||
skinparam titleFontName "Volkhov"
|
|
||||||
skinparam titleFontColor #14180C
|
|
||||||
skinparam ArrowColor #656F33
|
|
||||||
skinparam NoteBackgroundColor #DBEEDD
|
|
||||||
skinparam NoteFontColor #14180C
|
|
||||||
skinparam NoteBorderColor #4A5837
|
|
||||||
skinparam SwimlaneBorderColor #4A5837
|
|
||||||
skinparam SwimlaneBorderThickness 1
|
|
||||||
skinparam activityStartColor #EBECE3
|
|
||||||
skinparam activityEndColor #4A5837
|
|
||||||
skinparam activityStopColor #4A5837
|
|
||||||
skinparam ActivityBackgroundColor #EBECE3
|
|
||||||
skinparam ActivityBorderColor #4A5837
|
|
||||||
skinparam ActivityDiamondBackgroundColor #CBD2B5
|
|
||||||
skinparam ActivityDiamondBorderColor #4A5837
|
|
||||||
skinparam packageStyle rectangle
|
|
||||||
skinparam packageBackgroundColor #F1F3EA
|
|
||||||
skinparam packageBorderColor #4A5837
|
|
||||||
skinparam packageFontColor #14180C
|
|
||||||
skinparam classBackgroundColor #EBECE3
|
|
||||||
skinparam classBorderColor #4A5837
|
|
||||||
skinparam classFontColor #14180C
|
|
||||||
skinparam classAttributeFontColor #3F4724
|
|
||||||
skinparam classStereotypeFontColor #4A5837
|
|
||||||
skinparam interfaceBackgroundColor #DBEEDD
|
|
||||||
skinparam interfaceBorderColor #4A5837
|
|
||||||
skinparam interfaceFontColor #14180C
|
|
||||||
skinparam enumBackgroundColor #E4E6D8
|
|
||||||
skinparam enumBorderColor #4A5837
|
|
||||||
skinparam enumFontColor #14180C
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
@startuml biergarten_activity
|
|
||||||
!include ../biergarten-weizen-theme.puml
|
|
||||||
skinparam defaultFontSize 13
|
|
||||||
skinparam titleFontSize 20
|
|
||||||
|
|
||||||
title The Biergarten Data Pipeline — Activity Diagram
|
|
||||||
|
|
||||||
|Main|
|
|
||||||
start
|
|
||||||
:ParseArguments(argc, argv);
|
|
||||||
if (Invalid args?) then (yes)
|
|
||||||
:spdlog::error;
|
|
||||||
stop
|
|
||||||
else (no)
|
|
||||||
endif
|
|
||||||
:Init OpenSSL global state & LlamaBackendState;
|
|
||||||
:Build DI injector;
|
|
||||||
|
|
||||||
:Initialize SqliteExportService;
|
|
||||||
note right
|
|
||||||
Opens SQLite connection.
|
|
||||||
(Transactions are now managed
|
|
||||||
per-phase via batching).
|
|
||||||
end note
|
|
||||||
|
|
||||||
:Create BoundedChannel<LogEntry> log_ch;
|
|
||||||
:Spawn Log Worker thread;
|
|
||||||
note right
|
|
||||||
Log worker drains log_ch for the
|
|
||||||
entire pipeline lifetime.
|
|
||||||
All workers emit LogEntry structs
|
|
||||||
via PipelineLogger -- never spdlog directly.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:BiergartenPipelineOrchestrator::Run();
|
|
||||||
|BiergartenPipelineOrchestrator::Run()|
|
|
||||||
|
|
||||||
fork
|
|
||||||
:JsonLoader::LoadBeerStyles("beer-styles.json");
|
|
||||||
:EnrichmentService::PreWarmBeerStyleCache(beer_styles);
|
|
||||||
fork again
|
|
||||||
:JsonLoader::LoadLocations("locations.json");
|
|
||||||
:EnrichmentService::PreWarmLocationCache(sampled_locations);
|
|
||||||
end fork
|
|
||||||
fork
|
|
||||||
:JsonLoader::LoadNamesByCountry("names-by-country.json");
|
|
||||||
fork again
|
|
||||||
:JsonLoader::LoadPersonas("personas.json");
|
|
||||||
end fork
|
|
||||||
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
' PHASE 0 — USER GENERATION
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
|Orchestrator|
|
|
||||||
:RunUserPhase(sampled_locations);
|
|
||||||
:Create BoundedChannels\n(loc_ch, exp_ch);
|
|
||||||
|
|
||||||
fork
|
|
||||||
|Orchestrator|
|
|
||||||
:Loop: Send Locations -> loc_ch;
|
|
||||||
:Close loc_ch;
|
|
||||||
note right
|
|
||||||
Producer closes loc_ch.
|
|
||||||
LLM Worker while loop
|
|
||||||
terminates on empty + closed.
|
|
||||||
end note
|
|
||||||
fork again
|
|
||||||
|LLM Worker|
|
|
||||||
while (loc_ch has items?) is (yes)
|
|
||||||
:Receive Location;
|
|
||||||
|
|
||||||
:GetLocationContextFromCache(location);
|
|
||||||
note right
|
|
||||||
Guaranteed cache hit from startup.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:IPersonaSelectionStrategy::SelectPersona(\n personas_palette_);
|
|
||||||
note right
|
|
||||||
Guaranteed cache hit from startup.
|
|
||||||
Returns a Persona struct carrying
|
|
||||||
style_affinities, abv_range,
|
|
||||||
ibu_preference, checkin_weight.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:NamesByCountry::SampleName(\n location.iso3166_1);
|
|
||||||
note right
|
|
||||||
Deterministic lookup -- no LLM involved.
|
|
||||||
Name selected from pre-keyed table
|
|
||||||
and passed into the generation prompt.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:GenerateUser(enriched_city, persona, sampled_name)\nvia DataGenerator;
|
|
||||||
note right
|
|
||||||
LLM receives: EnrichedCity context + persona
|
|
||||||
description + sampled name. Generates
|
|
||||||
bio and preference signals grounded
|
|
||||||
in locale and persona.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:PipelineLogger::Log(Info, UserGeneration,\n city, user_id, "llm");
|
|
||||||
:Send GeneratedUser -> exp_ch;
|
|
||||||
endwhile (no)
|
|
||||||
:Close exp_ch;
|
|
||||||
note right
|
|
||||||
Producer closes exp_ch.
|
|
||||||
SQLite Worker while loop
|
|
||||||
terminates on empty + closed.
|
|
||||||
end note
|
|
||||||
fork again
|
|
||||||
|SQLite Worker|
|
|
||||||
:BEGIN TRANSACTION;
|
|
||||||
while (exp_ch has items?) is (yes)
|
|
||||||
:Receive GeneratedUser;
|
|
||||||
:ProcessUser(user);
|
|
||||||
:PipelineLogger::Log(Info, UserGeneration,\n city, user_id, "sqlite");
|
|
||||||
:Append -> user_pool_;
|
|
||||||
if (Batch size reached?) then (yes)
|
|
||||||
:COMMIT & BEGIN;
|
|
||||||
else (no)
|
|
||||||
endif
|
|
||||||
endwhile (no)
|
|
||||||
:COMMIT (Final);
|
|
||||||
end fork
|
|
||||||
|
|
||||||
|Orchestrator|
|
|
||||||
:Join LLM Worker, SQLite Worker;
|
|
||||||
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
' PHASE 1a — BREWERY GENERATION
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
:RunBreweryPhase(sampled_locations);
|
|
||||||
:Create BoundedChannels\n(loc_ch, exp_ch);
|
|
||||||
|
|
||||||
fork
|
|
||||||
|Orchestrator|
|
|
||||||
:Loop: Sample User from user_pool_
|
|
||||||
and pair with Location;
|
|
||||||
:Send BreweryTask(Location, User) -> loc_ch;
|
|
||||||
:Close loc_ch;
|
|
||||||
fork again
|
|
||||||
|LLM Worker|
|
|
||||||
while (loc_ch has items?) is (yes)
|
|
||||||
:Receive BreweryTask(Location, User);
|
|
||||||
|
|
||||||
:GetLocationContextFromCache(task.location);
|
|
||||||
note right
|
|
||||||
Guaranteed cache hit from startup.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:GenerateBrewery(enriched_city, context, task.user)\nvia DataGenerator;
|
|
||||||
note right
|
|
||||||
KV cache stays warm.
|
|
||||||
Brewery is linked to the sampled owner_user_id.
|
|
||||||
end note
|
|
||||||
:PipelineLogger::Log(Info,\n BreweryGeneration,\n city, brewery_id, "llm");
|
|
||||||
:Send GeneratedBrewery -> exp_ch;
|
|
||||||
endwhile (no)
|
|
||||||
:Close exp_ch;
|
|
||||||
fork again
|
|
||||||
|SQLite Worker|
|
|
||||||
:BEGIN TRANSACTION;
|
|
||||||
while (exp_ch has items?) is (yes)
|
|
||||||
:Receive GeneratedBrewery;
|
|
||||||
:ProcessBrewery(brewery);
|
|
||||||
:PipelineLogger::Log(Info,\n BreweryGeneration,\n city, brewery_id, "sqlite");
|
|
||||||
:Append -> brewery_pool_;
|
|
||||||
if (Batch size reached?) then (yes)
|
|
||||||
:COMMIT & BEGIN;
|
|
||||||
else (no)
|
|
||||||
endif
|
|
||||||
endwhile (no)
|
|
||||||
:COMMIT (Final);
|
|
||||||
end fork
|
|
||||||
|
|
||||||
|Orchestrator|
|
|
||||||
:Join LLM Worker, SQLite Worker;
|
|
||||||
note right
|
|
||||||
brewery_pool_ is now fully populated.
|
|
||||||
Phase 1b may begin.
|
|
||||||
end note
|
|
||||||
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
' PHASE 1b — BEER GENERATION
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
:RunBeerPhase();
|
|
||||||
:Create BoundedChannels\n(brew_ch, exp_ch);
|
|
||||||
|
|
||||||
fork
|
|
||||||
|Orchestrator|
|
|
||||||
:Loop: Send Breweries -> brew_ch;
|
|
||||||
:Close brew_ch;
|
|
||||||
fork again
|
|
||||||
|LLM Worker|
|
|
||||||
while (brew_ch has items?) is (yes)
|
|
||||||
:Receive GeneratedBrewery;
|
|
||||||
:IBeerSelectionStrategy::SelectStyles(\n brewery, beer_style_palette_);
|
|
||||||
|
|
||||||
while (For each selected BeerStyle?) is (remaining)
|
|
||||||
:GetStyleContextFromCache(style);
|
|
||||||
note right
|
|
||||||
Guaranteed cache hit from startup.
|
|
||||||
KV cache stays warm across all
|
|
||||||
beer generations -- system prompt
|
|
||||||
does not change within this phase.
|
|
||||||
end note
|
|
||||||
:GenerateBeer(brewery, style_context)\nvia DataGenerator;
|
|
||||||
:Attach GeneratedBeer to bundle;
|
|
||||||
endwhile (done)
|
|
||||||
|
|
||||||
:PipelineLogger::Log(Info,\n BeerGeneration,\n city, brewery_id, "llm");
|
|
||||||
:Send BeersBundle -> exp_ch;
|
|
||||||
endwhile (no)
|
|
||||||
:Close exp_ch;
|
|
||||||
fork again
|
|
||||||
|SQLite Worker|
|
|
||||||
:BEGIN TRANSACTION;
|
|
||||||
while (exp_ch has items?) is (yes)
|
|
||||||
:Receive BeersBundle;
|
|
||||||
while (For each beer in bundle?) is (remaining)
|
|
||||||
:Set beer.brewery_id from bundle;
|
|
||||||
:ProcessBeer(beer);
|
|
||||||
:Append -> beer_pool_;
|
|
||||||
endwhile (done)
|
|
||||||
:PipelineLogger::Log(Info,\n BeerGeneration,\n city, brewery_id, "sqlite");
|
|
||||||
if (Batch size reached?) then (yes)
|
|
||||||
:COMMIT & BEGIN;
|
|
||||||
else (no)
|
|
||||||
endif
|
|
||||||
endwhile (no)
|
|
||||||
:COMMIT (Final);
|
|
||||||
end fork
|
|
||||||
|
|
||||||
|Orchestrator|
|
|
||||||
:Join LLM Worker, SQLite Worker;
|
|
||||||
note right
|
|
||||||
Both brewery_pool_ and beer_pool_
|
|
||||||
are now completely populated.
|
|
||||||
Checkin and Follow phases may
|
|
||||||
now run in parallel.
|
|
||||||
end note
|
|
||||||
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
' PHASE 2 — CHECKIN + FOLLOW GENERATION
|
|
||||||
' (parallel — both depend only on user_pool_
|
|
||||||
' and brewery_pool_ being fully populated)
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
fork
|
|
||||||
|Orchestrator|
|
|
||||||
:RunCheckinPhase();
|
|
||||||
:ICheckinDistributionStrategy::\nAssignActivityWeights(user_pool_);
|
|
||||||
note right
|
|
||||||
Weights seeded from each user's
|
|
||||||
persona.checkin_weight. J-curve profile
|
|
||||||
emerges from persona distribution.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:BEGIN TRANSACTION;
|
|
||||||
while (For each GeneratedUser in user_pool_?) is (remaining)
|
|
||||||
:CheckinsForUser(user, brewery_pool_.size());
|
|
||||||
while (For each checkin index?) is (remaining)
|
|
||||||
:TimestampFor(user, index);
|
|
||||||
:Select brewery from brewery_pool_;
|
|
||||||
:GenerateCheckin(user, brewery, timestamp)\nvia DataGenerator;
|
|
||||||
:ProcessCheckin(checkin);
|
|
||||||
:PipelineLogger::Log(Info, CheckinGeneration,\n nullopt, checkin_id, "sqlite");
|
|
||||||
:Append -> checkin_pool_;
|
|
||||||
if (Batch size reached?) then (yes)
|
|
||||||
:COMMIT & BEGIN;
|
|
||||||
else (no)
|
|
||||||
endif
|
|
||||||
endwhile (done)
|
|
||||||
endwhile (done)
|
|
||||||
:COMMIT (Final);
|
|
||||||
|
|
||||||
fork again
|
|
||||||
|Orchestrator|
|
|
||||||
:RunFollowPhase();
|
|
||||||
:IFollowGenerationStrategy::\nAssignFollowWeights(user_pool_);
|
|
||||||
note right
|
|
||||||
For RandomFollowStrategy, weights
|
|
||||||
are uniform. For ActivityWeightedFollowStrategy,
|
|
||||||
weights derived from user.activity_weight
|
|
||||||
so high-activity users attract more followers.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:BEGIN TRANSACTION;
|
|
||||||
:IFollowGenerationStrategy::\nGenerateFollows(user_pool_);
|
|
||||||
note right
|
|
||||||
Self-follow constraint (follower_id != followed_id)
|
|
||||||
enforced here and at the DB schema level.
|
|
||||||
end note
|
|
||||||
while (For each GeneratedFollow?) is (remaining)
|
|
||||||
:ProcessFollow(follow);
|
|
||||||
:PipelineLogger::Log(Info, FollowGeneration,\n nullopt, follower_id, "sqlite");
|
|
||||||
:Append -> follow_pool_;
|
|
||||||
if (Batch size reached?) then (yes)
|
|
||||||
:COMMIT & BEGIN;
|
|
||||||
else (no)
|
|
||||||
endif
|
|
||||||
endwhile (done)
|
|
||||||
:COMMIT (Final);
|
|
||||||
|
|
||||||
end fork
|
|
||||||
|
|
||||||
|Orchestrator|
|
|
||||||
:Join CheckinPhase, FollowPhase;
|
|
||||||
note right
|
|
||||||
checkin_pool_ and follow_pool_
|
|
||||||
are now fully populated.
|
|
||||||
Rating phase may begin.
|
|
||||||
end note
|
|
||||||
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
' PHASE 3 — RATING GENERATION
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
:RunRatingPhase();
|
|
||||||
note right
|
|
||||||
Beer selection biased by
|
|
||||||
user.persona.style_affinities and abv_range.
|
|
||||||
Rating skew modulated per persona.
|
|
||||||
end note
|
|
||||||
|
|
||||||
:BEGIN TRANSACTION;
|
|
||||||
while (For each GeneratedCheckin in checkin_pool_?) is (remaining)
|
|
||||||
:Match brewery_id, select beer from beer_pool_\n(same brewery_id, biased by persona affinities);
|
|
||||||
if (Beer exists for brewery?) then (yes)
|
|
||||||
:GenerateRating(user, beer, checkin_id)\nvia DataGenerator;
|
|
||||||
:ProcessRating(rating);
|
|
||||||
:PipelineLogger::Log(Info, RatingGeneration,\n nullopt, rating_id, "sqlite");
|
|
||||||
if (Batch size reached?) then (yes)
|
|
||||||
:COMMIT & BEGIN;
|
|
||||||
else (no)
|
|
||||||
endif
|
|
||||||
else (no)
|
|
||||||
:PipelineLogger::Log(Warn, RatingGeneration,\n nullopt, brewery_id, "sqlite");
|
|
||||||
:Skip -- brewery has no beers;
|
|
||||||
endif
|
|
||||||
endwhile (done)
|
|
||||||
:COMMIT (Final);
|
|
||||||
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
' TEARDOWN
|
|
||||||
' ═══════════════════════════════════════════
|
|
||||||
|Orchestrator|
|
|
||||||
:Finalize SqliteExportService;
|
|
||||||
note right
|
|
||||||
Safely closes the DB connection.
|
|
||||||
end note
|
|
||||||
:Close log_ch;
|
|
||||||
|
|
||||||
|Main|
|
|
||||||
:spdlog::info "Pipeline complete in X ms";
|
|
||||||
:Join Log Worker;
|
|
||||||
note right
|
|
||||||
Drain guarantees no LogEntry is
|
|
||||||
dropped at shutdown.
|
|
||||||
end note
|
|
||||||
stop
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +1,6 @@
|
|||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
This document describes the testing strategy and how to run tests for The
|
This document describes the testing strategy and how to run tests for The Biergarten App.
|
||||||
Biergarten App.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -10,15 +9,13 @@ The project uses a multi-layered testing approach across backend and frontend:
|
|||||||
- **API.Specs** - BDD integration tests using Reqnroll (Gherkin)
|
- **API.Specs** - BDD integration tests using Reqnroll (Gherkin)
|
||||||
- **Infrastructure.Repository.Tests** - Unit tests for data access layer
|
- **Infrastructure.Repository.Tests** - Unit tests for data access layer
|
||||||
- **Service.Auth.Tests** - Unit tests for authentication business logic
|
- **Service.Auth.Tests** - Unit tests for authentication business logic
|
||||||
- **Storybook Vitest project** - Browser-based interaction tests for shared
|
- **Storybook Vitest project** - Browser-based interaction tests for shared website stories
|
||||||
website stories
|
- **Storybook Playwright suite** - Browser checks against Storybook-rendered components
|
||||||
- **Storybook Playwright suite** - Browser checks against Storybook-rendered
|
|
||||||
components
|
|
||||||
|
|
||||||
## Running Tests with Docker (Recommended)
|
## Running Tests with Docker (Recommended)
|
||||||
|
|
||||||
The easiest way to run all tests is using Docker Compose, which sets up an
|
The easiest way to run all tests is using Docker Compose, which sets up an isolated test
|
||||||
isolated test environment:
|
environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
||||||
@@ -101,8 +98,7 @@ npm run test:storybook
|
|||||||
|
|
||||||
**Purpose**:
|
**Purpose**:
|
||||||
|
|
||||||
- Verifies shared stories such as form fields, submit buttons, navbar states,
|
- Verifies shared stories such as form fields, submit buttons, navbar states, toasts, and the theme gallery
|
||||||
toasts, and the theme gallery
|
|
||||||
- Runs in browser mode via Vitest and Storybook integration
|
- Runs in browser mode via Vitest and Storybook integration
|
||||||
|
|
||||||
### Frontend Playwright Storybook Tests
|
### Frontend Playwright Storybook Tests
|
||||||
@@ -117,8 +113,7 @@ npm run test:storybook:playwright
|
|||||||
|
|
||||||
- Storybook dependencies installed
|
- Storybook dependencies installed
|
||||||
- Playwright browser dependencies installed
|
- Playwright browser dependencies installed
|
||||||
- The command will start or reuse the Storybook server defined in
|
- The command will start or reuse the Storybook server defined in `playwright.storybook.config.ts`
|
||||||
`playwright.storybook.config.ts`
|
|
||||||
|
|
||||||
## Test Coverage
|
## Test Coverage
|
||||||
|
|
||||||
@@ -283,8 +278,7 @@ Scenario: User login with valid credentials
|
|||||||
|
|
||||||
## Continuous Integration
|
## Continuous Integration
|
||||||
|
|
||||||
Tests run automatically in CI/CD pipelines using the test Docker Compose
|
Tests run automatically in CI/CD pipelines using the test Docker Compose configuration:
|
||||||
configuration:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# CI/CD command
|
# CI/CD command
|
||||||
@@ -298,8 +292,7 @@ Exit codes:
|
|||||||
- `0` - All tests passed
|
- `0` - All tests passed
|
||||||
- Non-zero - Test failures occurred
|
- Non-zero - Test failures occurred
|
||||||
|
|
||||||
Frontend UI checks should also be included in CI for the active website
|
Frontend UI checks should also be included in CI for the active website workspace:
|
||||||
workspace:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd src/Website
|
cd src/Website
|
||||||
@@ -2,14 +2,11 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The Core project implements comprehensive JWT token validation across three
|
The Core project implements comprehensive JWT token validation across three token types:
|
||||||
token types:
|
|
||||||
|
|
||||||
- **Access Tokens**: Short-lived (1 hour) tokens for API authentication
|
- **Access Tokens**: Short-lived (1 hour) tokens for API authentication
|
||||||
- **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access
|
- **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access tokens
|
||||||
tokens
|
- **Confirmation Tokens**: Short-lived (30 minutes) tokens for email confirmation
|
||||||
- **Confirmation Tokens**: Short-lived (30 minutes) tokens for email
|
|
||||||
confirmation
|
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
@@ -20,13 +17,10 @@ token types:
|
|||||||
Low-level JWT operations.
|
Low-level JWT operations.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
- `GenerateJwt()` - Creates signed JWT tokens
|
- `GenerateJwt()` - Creates signed JWT tokens
|
||||||
- `ValidateJwtAsync()` - Validates token signature, expiration, and format
|
- `ValidateJwtAsync()` - Validates token signature, expiration, and format
|
||||||
|
|
||||||
**Implementation:**
|
**Implementation:** [JwtInfrastructure.cs](Infrastructure.Jwt/JwtInfrastructure.cs)
|
||||||
[JwtInfrastructure.cs](Infrastructure.Jwt/JwtInfrastructure.cs)
|
|
||||||
|
|
||||||
- Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
|
- Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
|
||||||
- Algorithm: HS256 (HMAC-SHA256)
|
- Algorithm: HS256 (HMAC-SHA256)
|
||||||
- Validates token lifetime, signature, and well-formedness
|
- Validates token lifetime, signature, and well-formedness
|
||||||
@@ -38,20 +32,16 @@ Low-level JWT operations.
|
|||||||
High-level token validation with context (token type, user extraction).
|
High-level token validation with context (token type, user extraction).
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
- `ValidateAccessTokenAsync(string token)` - Validates access tokens
|
- `ValidateAccessTokenAsync(string token)` - Validates access tokens
|
||||||
- `ValidateRefreshTokenAsync(string token)` - Validates refresh tokens
|
- `ValidateRefreshTokenAsync(string token)` - Validates refresh tokens
|
||||||
- `ValidateConfirmationTokenAsync(string token)` - Validates confirmation tokens
|
- `ValidateConfirmationTokenAsync(string token)` - Validates confirmation tokens
|
||||||
|
|
||||||
**Returns:** `ValidatedToken` record containing:
|
**Returns:** `ValidatedToken` record containing:
|
||||||
|
|
||||||
- `UserId` (Guid)
|
- `UserId` (Guid)
|
||||||
- `Username` (string)
|
- `Username` (string)
|
||||||
- `Principal` (ClaimsPrincipal) - Full JWT claims
|
- `Principal` (ClaimsPrincipal) - Full JWT claims
|
||||||
|
|
||||||
**Implementation:**
|
**Implementation:** [TokenValidationService.cs](Service.Auth/TokenValidationService.cs)
|
||||||
[TokenValidationService.cs](Service.Auth/TokenValidationService.cs)
|
|
||||||
|
|
||||||
- Reads token secrets from environment variables
|
- Reads token secrets from environment variables
|
||||||
- Extracts and validates claims (Sub, UniqueName)
|
- Extracts and validates claims (Sub, UniqueName)
|
||||||
- Throws `UnauthorizedException` on validation failure
|
- Throws `UnauthorizedException` on validation failure
|
||||||
@@ -61,18 +51,15 @@ High-level token validation with context (token type, user extraction).
|
|||||||
Token generation (existing service extended).
|
Token generation (existing service extended).
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
- `GenerateAccessToken(UserAccount)` - Creates 1-hour access token
|
- `GenerateAccessToken(UserAccount)` - Creates 1-hour access token
|
||||||
- `GenerateRefreshToken(UserAccount)` - Creates 21-day refresh token
|
- `GenerateRefreshToken(UserAccount)` - Creates 21-day refresh token
|
||||||
- `GenerateConfirmationToken(UserAccount)` - Creates 30-minute confirmation
|
- `GenerateConfirmationToken(UserAccount)` - Creates 30-minute confirmation token
|
||||||
token
|
|
||||||
|
|
||||||
### Integration Points
|
### Integration Points
|
||||||
|
|
||||||
#### [ConfirmationService](Service.Auth/IConfirmationService.cs)
|
#### [ConfirmationService](Service.Auth/IConfirmationService.cs)
|
||||||
|
|
||||||
**Flow:**
|
**Flow:**
|
||||||
|
|
||||||
1. Receives confirmation token from user
|
1. Receives confirmation token from user
|
||||||
2. Calls `TokenValidationService.ValidateConfirmationTokenAsync()`
|
2. Calls `TokenValidationService.ValidateConfirmationTokenAsync()`
|
||||||
3. Extracts user ID from validated token
|
3. Extracts user ID from validated token
|
||||||
@@ -82,7 +69,6 @@ Token generation (existing service extended).
|
|||||||
#### [RefreshTokenService](Service.Auth/RefreshTokenService.cs)
|
#### [RefreshTokenService](Service.Auth/RefreshTokenService.cs)
|
||||||
|
|
||||||
**Flow:**
|
**Flow:**
|
||||||
|
|
||||||
1. Receives refresh token from user
|
1. Receives refresh token from user
|
||||||
2. Calls `TokenValidationService.ValidateRefreshTokenAsync()`
|
2. Calls `TokenValidationService.ValidateRefreshTokenAsync()`
|
||||||
3. Retrieves user account via `AuthRepository.GetUserByIdAsync()`
|
3. Retrieves user account via `AuthRepository.GetUserByIdAsync()`
|
||||||
@@ -92,7 +78,6 @@ Token generation (existing service extended).
|
|||||||
#### [AuthController](API.Core/Controllers/AuthController.cs)
|
#### [AuthController](API.Core/Controllers/AuthController.cs)
|
||||||
|
|
||||||
**Endpoints:**
|
**Endpoints:**
|
||||||
|
|
||||||
- `POST /api/auth/register` - Register new user
|
- `POST /api/auth/register` - Register new user
|
||||||
- `POST /api/auth/login` - Authenticate user
|
- `POST /api/auth/login` - Authenticate user
|
||||||
- `POST /api/auth/confirm?token=...` - Confirm email
|
- `POST /api/auth/confirm?token=...` - Confirm email
|
||||||
@@ -103,13 +88,11 @@ Token generation (existing service extended).
|
|||||||
### Token Secrets
|
### Token Secrets
|
||||||
|
|
||||||
Three independent secrets enable:
|
Three independent secrets enable:
|
||||||
|
|
||||||
- **Key rotation** - Rotate each secret type independently
|
- **Key rotation** - Rotate each secret type independently
|
||||||
- **Isolation** - Compromise of one secret doesn't affect others
|
- **Isolation** - Compromise of one secret doesn't affect others
|
||||||
- **Different expiration** - Different token types can expire at different rates
|
- **Different expiration** - Different token types can expire at different rates
|
||||||
|
|
||||||
**Environment Variables:**
|
**Environment Variables:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ACCESS_TOKEN_SECRET=... # Signs 1-hour access tokens
|
ACCESS_TOKEN_SECRET=... # Signs 1-hour access tokens
|
||||||
REFRESH_TOKEN_SECRET=... # Signs 21-day refresh tokens
|
REFRESH_TOKEN_SECRET=... # Signs 21-day refresh tokens
|
||||||
@@ -128,7 +111,6 @@ Each token is validated for:
|
|||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
Validation failures return HTTP 401 Unauthorized:
|
Validation failures return HTTP 401 Unauthorized:
|
||||||
|
|
||||||
- Invalid signature → "Invalid token"
|
- Invalid signature → "Invalid token"
|
||||||
- Expired token → "Invalid token" (message doesn't reveal reason for security)
|
- Expired token → "Invalid token" (message doesn't reveal reason for security)
|
||||||
- Missing claims → "Invalid token"
|
- Missing claims → "Invalid token"
|
||||||
@@ -167,19 +149,16 @@ Validation failures return HTTP 401 Unauthorized:
|
|||||||
### Unit Tests
|
### Unit Tests
|
||||||
|
|
||||||
**TokenValidationService.test.cs**
|
**TokenValidationService.test.cs**
|
||||||
|
|
||||||
- Happy path: Valid token extraction
|
- Happy path: Valid token extraction
|
||||||
- Error cases: Invalid, expired, malformed tokens
|
- Error cases: Invalid, expired, malformed tokens
|
||||||
- Missing/invalid claims scenarios
|
- Missing/invalid claims scenarios
|
||||||
|
|
||||||
**RefreshTokenService.test.cs**
|
**RefreshTokenService.test.cs**
|
||||||
|
|
||||||
- Successful refresh with valid token
|
- Successful refresh with valid token
|
||||||
- Invalid/expired refresh token rejection
|
- Invalid/expired refresh token rejection
|
||||||
- Non-existent user handling
|
- Non-existent user handling
|
||||||
|
|
||||||
**ConfirmationService.test.cs**
|
**ConfirmationService.test.cs**
|
||||||
|
|
||||||
- Successful confirmation with valid token
|
- Successful confirmation with valid token
|
||||||
- Token validation failures
|
- Token validation failures
|
||||||
- User not found scenarios
|
- User not found scenarios
|
||||||
@@ -187,19 +166,16 @@ Validation failures return HTTP 401 Unauthorized:
|
|||||||
### BDD Tests (Reqnroll)
|
### BDD Tests (Reqnroll)
|
||||||
|
|
||||||
**TokenRefresh.feature**
|
**TokenRefresh.feature**
|
||||||
|
|
||||||
- Successful token refresh
|
- Successful token refresh
|
||||||
- Invalid/expired token rejection
|
- Invalid/expired token rejection
|
||||||
- Missing token validation
|
- Missing token validation
|
||||||
|
|
||||||
**Confirmation.feature**
|
**Confirmation.feature**
|
||||||
|
|
||||||
- Successful email confirmation
|
- Successful email confirmation
|
||||||
- Expired/tampered token rejection
|
- Expired/tampered token rejection
|
||||||
- Missing token validation
|
- Missing token validation
|
||||||
|
|
||||||
**AccessTokenValidation.feature**
|
**AccessTokenValidation.feature**
|
||||||
|
|
||||||
- Protected endpoint access token validation
|
- Protected endpoint access token validation
|
||||||
- Invalid/expired access token rejection
|
- Invalid/expired access token rejection
|
||||||
- Token type mismatch (refresh used as access token)
|
- Token type mismatch (refresh used as access token)
|
||||||
2411
misc/raw-data/beers.csv
Normal file
2411
misc/raw-data/beers.csv
Normal file
File diff suppressed because it is too large
Load Diff
559
misc/raw-data/breweries.csv
Normal file
559
misc/raw-data/breweries.csv
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
,name,city,state
|
||||||
|
0,NorthGate Brewing ,Minneapolis, MN
|
||||||
|
1,Against the Grain Brewery,Louisville, KY
|
||||||
|
2,Jack's Abby Craft Lagers,Framingham, MA
|
||||||
|
3,Mike Hess Brewing Company,San Diego, CA
|
||||||
|
4,Fort Point Beer Company,San Francisco, CA
|
||||||
|
5,COAST Brewing Company,Charleston, SC
|
||||||
|
6,Great Divide Brewing Company,Denver, CO
|
||||||
|
7,Tapistry Brewing,Bridgman, MI
|
||||||
|
8,Big Lake Brewing,Holland, MI
|
||||||
|
9,The Mitten Brewing Company,Grand Rapids, MI
|
||||||
|
10,Brewery Vivant,Grand Rapids, MI
|
||||||
|
11,Petoskey Brewing,Petoskey, MI
|
||||||
|
12,Blackrocks Brewery,Marquette, MI
|
||||||
|
13,Perrin Brewing Company,Comstock Park, MI
|
||||||
|
14,Witch's Hat Brewing Company,South Lyon, MI
|
||||||
|
15,Founders Brewing Company,Grand Rapids, MI
|
||||||
|
16,Flat 12 Bierwerks,Indianapolis, IN
|
||||||
|
17,Tin Man Brewing Company,Evansville, IN
|
||||||
|
18,Black Acre Brewing Co.,Indianapolis, IN
|
||||||
|
19,Brew Link Brewing,Plainfield, IN
|
||||||
|
20,Bare Hands Brewery,Granger, IN
|
||||||
|
21,Three Pints Brewing,Martinsville, IN
|
||||||
|
22,Four Fathers Brewing ,Valparaiso, IN
|
||||||
|
23,Indiana City Brewing,Indianapolis, IN
|
||||||
|
24,Burn 'Em Brewing,Michigan City, IN
|
||||||
|
25,Sun King Brewing Company,Indianapolis, IN
|
||||||
|
26,Evil Czech Brewery,Mishawaka, IN
|
||||||
|
27,450 North Brewing Company,Columbus, IN
|
||||||
|
28,Taxman Brewing Company,Bargersville, IN
|
||||||
|
29,Cedar Creek Brewery,Seven Points, TX
|
||||||
|
30,SanTan Brewing Company,Chandler, AZ
|
||||||
|
31,Boulevard Brewing Company,Kansas City, MO
|
||||||
|
32,James Page Brewing Company,Stevens Point, WI
|
||||||
|
33,The Dudes' Brewing Company,Torrance, CA
|
||||||
|
34,Ballast Point Brewing Company,San Diego, CA
|
||||||
|
35,Anchor Brewing Company,San Francisco, CA
|
||||||
|
36,Figueroa Mountain Brewing Company,Buellton, CA
|
||||||
|
37,Avery Brewing Company,Boulder, CO
|
||||||
|
38,Twisted X Brewing Company,Dripping Springs, TX
|
||||||
|
39,Gonzo's BiggDogg Brewing,Kalamazoo, MI
|
||||||
|
40,Big Muddy Brewing,Murphysboro, IL
|
||||||
|
41,Lost Nation Brewing,East Fairfield, VT
|
||||||
|
42,Rising Tide Brewing Company,Portland, ME
|
||||||
|
43,Rivertowne Brewing Company,Export, PA
|
||||||
|
44,Revolution Brewing Company,Chicago, IL
|
||||||
|
45,Tallgrass Brewing Company,Manhattan, KS
|
||||||
|
46,Sixpoint Craft Ales,Brooklyn, NY
|
||||||
|
47,White Birch Brewing,Hooksett, NH
|
||||||
|
48,Firestone Walker Brewing Company,Paso Robles, CA
|
||||||
|
49,SweetWater Brewing Company,Atlanta, GA
|
||||||
|
50,Flying Mouse Brewery,Troutville, VA
|
||||||
|
51,Upslope Brewing Company,Boulder, CO
|
||||||
|
52,Pipeworks Brewing Company,Chicago, IL
|
||||||
|
53,Bent Brewstillery,Roseville, MN
|
||||||
|
54,Flesk Brewing Company,Lombard, IL
|
||||||
|
55,Pollyanna Brewing Company,Lemont, IL
|
||||||
|
56,BuckleDown Brewing,Lyons, IL
|
||||||
|
57,Destihl Brewery,Bloomington, IL
|
||||||
|
58,Summit Brewing Company,St. Paul, MN
|
||||||
|
59,Latitude 42 Brewing Company,Portage, MI
|
||||||
|
60,4 Hands Brewing Company,Saint Louis, MO
|
||||||
|
61,Surly Brewing Company,Brooklyn Center, MN
|
||||||
|
62,Against The Grain Brewery,Louisville, KY
|
||||||
|
63,Crazy Mountain Brewing Company,Edwards, CO
|
||||||
|
64,SlapShot Brewing Company,Chicago, IL
|
||||||
|
65,Mikerphone Brewing,Chicago, IL
|
||||||
|
66,Freetail Brewing Company,San Antonio, TX
|
||||||
|
67,3 Daughters Brewing,St Petersburg, FL
|
||||||
|
68,Red Shedman Farm Brewery and Hop...,Mt. Airy, MD
|
||||||
|
69,Appalachian Mountain Brewery,Boone, NC
|
||||||
|
70,Birdsong Brewing Company,Charlotte, NC
|
||||||
|
71,Union Craft Brewing,Baltimore, MD
|
||||||
|
72,Atwater Brewery,Detroit, MI
|
||||||
|
73,Ale Asylum,Madison, WI
|
||||||
|
74,Two Brothers Brewing Company,Warrenville, IL
|
||||||
|
75,Bent Paddle Brewing Company,Duluth, MN
|
||||||
|
76,Bell's Brewery,Kalamazoo, MI
|
||||||
|
77,Blue Owl Brewing,Austin, TX
|
||||||
|
78,Speakasy Ales & Lagers,San Francisco, CA
|
||||||
|
79,Black Tooth Brewing Company,Sheridan, WY
|
||||||
|
80,Hopworks Urban Brewery,Portland, OR
|
||||||
|
81,Epic Brewing,Denver, CO
|
||||||
|
82,New Belgium Brewing Company,Fort Collins, CO
|
||||||
|
83,Sierra Nevada Brewing Company,Chico, CA
|
||||||
|
84,Keweenaw Brewing Company,Houghton, MI
|
||||||
|
85,Brewery Terra Firma,Traverse City, MI
|
||||||
|
86,Grey Sail Brewing Company,Westerly, RI
|
||||||
|
87,Kirkwood Station Brewing Company,Kirkwood, MO
|
||||||
|
88,Goose Island Brewing Company,Chicago, IL
|
||||||
|
89,Broad Brook Brewing LLC,East Windsor, CT
|
||||||
|
90,The Lion Brewery,Wilkes-Barre, PA
|
||||||
|
91,Madtree Brewing Company,Cincinnati, OH
|
||||||
|
92,Jackie O's Pub & Brewery,Athens, OH
|
||||||
|
93,Rhinegeist Brewery,Cincinnati, OH
|
||||||
|
94,Warped Wing Brewing Company,Dayton, OH
|
||||||
|
95,Blackrocks Brewery,Marquette, MA
|
||||||
|
96,Catawba Valley Brewing Company,Morganton, NC
|
||||||
|
97,Tröegs Brewing Company,Hershey, PA
|
||||||
|
98,Mission Brewery,San Diego, CA
|
||||||
|
99,Christian Moerlein Brewing Company,Cincinnati, OH
|
||||||
|
100,West Sixth Brewing,Lexington, KY
|
||||||
|
101,Coastal Extreme Brewing Company,Newport, RI
|
||||||
|
102,King Street Brewing Company,Anchorage, AK
|
||||||
|
103,Beer Works Brewery,Lowell, MA
|
||||||
|
104,Lone Tree Brewing Company,Lone Tree, CO
|
||||||
|
105,Four String Brewing Company,Columbus, OH
|
||||||
|
106,Glabrous Brewing Company,Pineland, ME
|
||||||
|
107,Bonfire Brewing Company,Eagle, CO
|
||||||
|
108,Thomas Hooker Brewing Company,Bloomfield, CT
|
||||||
|
109,"Woodstock Inn, Station & Brewery",North Woodstock, NH
|
||||||
|
110,Renegade Brewing Company,Denver, CO
|
||||||
|
111,Mother Earth Brew Company,Vista, CA
|
||||||
|
112,Black Market Brewing Company,Temecula, CA
|
||||||
|
113,Vault Brewing Company,Yardley, PA
|
||||||
|
114,Jailbreak Brewing Company,Laurel, MD
|
||||||
|
115,Smartmouth Brewing Company,Norfolk, VA
|
||||||
|
116,Base Camp Brewing Co.,Portland, OR
|
||||||
|
117,Alameda Brewing,Portland, OR
|
||||||
|
118,Southern Star Brewing Company,Conroe, TX
|
||||||
|
119,Steamworks Brewing Company,Durango, CO
|
||||||
|
120,Horny Goat Brew Pub,Milwaukee, WI
|
||||||
|
121,Cheboygan Brewing Company,Cheboygan, MI
|
||||||
|
122,Center of the Universe Brewing C...,Ashland, VA
|
||||||
|
123,Ipswich Ale Brewery,Ipswich, MA
|
||||||
|
124,Griffin Claw Brewing Company,Birmingham, MI
|
||||||
|
125,Karbach Brewing Company,Houston, TX
|
||||||
|
126,Uncle Billy's Brewery and Smokeh...,Austin, TX
|
||||||
|
127,Deep Ellum Brewing Company,Dallas, TX
|
||||||
|
128,Real Ale Brewing Company,Blanco, TX
|
||||||
|
129,Straub Brewery,St Mary's, PA
|
||||||
|
130,Shebeen Brewing Company,Wolcott, CT
|
||||||
|
131,Stevens Point Brewery,Stevens Point, WI
|
||||||
|
132,Weston Brewing Company,Weston, MO
|
||||||
|
133,Southern Prohibition Brewing Com...,Hattiesburg, MS
|
||||||
|
134,Minhas Craft Brewery,Monroe, WI
|
||||||
|
135,Pug Ryan's Brewery,Dillon, CO
|
||||||
|
136,Hops & Grains Brewing Company,Austin, TX
|
||||||
|
137,Sietsema Orchards and Cider Mill,Ada, MI
|
||||||
|
138,Summit Brewing Company,St Paul, MN
|
||||||
|
139,Core Brewing & Distilling Company,Springdale, AR
|
||||||
|
140,Independence Brewing Company,Austin, TX
|
||||||
|
141,Cigar City Brewing Company,Tampa, FL
|
||||||
|
142,Third Street Brewhouse,Cold Spring, MN
|
||||||
|
143,Narragansett Brewing Company,Providence, RI
|
||||||
|
144,Grimm Brothers Brewhouse,Loveland, CO
|
||||||
|
145,Cisco Brewers,Nantucket, MA
|
||||||
|
146,Angry Minnow,Hayward, WI
|
||||||
|
147,Platform Beer Company,Cleveland, OH
|
||||||
|
148,Odyssey Beerwerks,Arvada, CO
|
||||||
|
149,Lonerider Brewing Company,Raleigh, NC
|
||||||
|
150,Oakshire Brewing,Eugene, OR
|
||||||
|
151,Fort Pitt Brewing Company,Latrobe, PA
|
||||||
|
152,Tin Roof Brewing Company,Baton Rouge, LA
|
||||||
|
153,Three Creeks Brewing,Sisters, OR
|
||||||
|
154,2 Towns Ciderhouse,Corvallis, OR
|
||||||
|
155,Caldera Brewing Company,Ashland, OR
|
||||||
|
156,Greenbrier Valley Brewing Company,Lewisburg, WV
|
||||||
|
157,Phoenix Ale Brewery,Phoenix, AZ
|
||||||
|
158,Lumberyard Brewing Company,Flagstaff, AZ
|
||||||
|
159,Uinta Brewing Company,Salt Lake City, UT
|
||||||
|
160,Four Peaks Brewing Company,Tempe, AZ
|
||||||
|
161,Martin House Brewing Company,Fort Worth, TX
|
||||||
|
162,Right Brain Brewery,Traverse City, MI
|
||||||
|
163,Sly Fox Brewing Company,Phoenixville, PA
|
||||||
|
164,Round Guys Brewing,Lansdale, PA
|
||||||
|
165,Great Crescent Brewery,Aurora, IN
|
||||||
|
166,Oskar Blues Brewery,Longmont, CO
|
||||||
|
167,Boxcar Brewing Company,West Chester, PA
|
||||||
|
168,High Hops Brewery,Windsor, CO
|
||||||
|
169,Crooked Fence Brewing Company,Garden City, ID
|
||||||
|
170,Everybody's Brewing,White Salmon, WA
|
||||||
|
171,Anderson Valley Brewing Company,Boonville, CA
|
||||||
|
172,Fiddlehead Brewing Company,Shelburne, VT
|
||||||
|
173,Evil Twin Brewing,Brooklyn, NY
|
||||||
|
174,New Orleans Lager & Ale Brewing ...,New Orleans, LA
|
||||||
|
175,Spiteful Brewing Company,Chicago, IL
|
||||||
|
176,Rahr & Sons Brewing Company,Fort Worth, TX
|
||||||
|
177,18th Street Brewery,Gary, IN
|
||||||
|
178,Cambridge Brewing Company,Cambridge, MA
|
||||||
|
179,Carolina Brewery,Pittsboro, NC
|
||||||
|
180,Frog Level Brewing Company,Waynesville, NC
|
||||||
|
181,Wild Wolf Brewing Company,Nellysford, VA
|
||||||
|
182,COOP Ale Works,Oklahoma City, OK
|
||||||
|
183,Seventh Son Brewing Company,Columbus, OH
|
||||||
|
184,Oasis Texas Brewing Company,Austin, TX
|
||||||
|
185,Vander Mill Ciders,Spring Lake, MI
|
||||||
|
186,St. Julian Winery,Paw Paw, MI
|
||||||
|
187,Pedernales Brewing Company,Fredericksburg, TX
|
||||||
|
188,Mother's Brewing,Springfield, MO
|
||||||
|
189,Modern Monks Brewery,Lincoln, NE
|
||||||
|
190,Two Beers Brewing Company,Seattle, WA
|
||||||
|
191,Snake River Brewing Company,Jackson, WY
|
||||||
|
192,Capital Brewery,Middleton, WI
|
||||||
|
193,Anthem Brewing Company,Oklahoma City, OK
|
||||||
|
194,Goodlife Brewing Co.,Bend, OR
|
||||||
|
195,Breakside Brewery,Portland, OR
|
||||||
|
196,Goose Island Brewery Company,Chicago, IL
|
||||||
|
197,Burnside Brewing Co.,Portland, OR
|
||||||
|
198,Hop Valley Brewing Company,Springfield, OR
|
||||||
|
199,Worthy Brewing Company,Bend, OR
|
||||||
|
200,Occidental Brewing Company,Portland, OR
|
||||||
|
201,Fearless Brewing Company,Estacada, OR
|
||||||
|
202,Upland Brewing Company,Bloomington, IN
|
||||||
|
203,Mehana Brewing Co.,Hilo, HI
|
||||||
|
204,Hawai'i Nui Brewing Co.,Hilo, HI
|
||||||
|
205,People's Brewing Company,Lafayette, IN
|
||||||
|
206,Fort George Brewery,Astoria, OR
|
||||||
|
207,Branchline Brewing Company,San Antonio, TX
|
||||||
|
208,Kalona Brewing Company,Kalona, IA
|
||||||
|
209,Modern Times Beer,San Diego, CA
|
||||||
|
210,Temperance Beer Company,Evanston, IL
|
||||||
|
211,Wisconsin Brewing Company,Verona, WI
|
||||||
|
212,Crow Peak Brewing Company,Spearfish, SD
|
||||||
|
213,Grapevine Craft Brewery,Farmers Branch, TX
|
||||||
|
214,Buffalo Bayou Brewing Company,Houston, TX
|
||||||
|
215,Texian Brewing Co.,Richmond, TX
|
||||||
|
216,Orpheus Brewing,Atlanta, GA
|
||||||
|
217,Forgotten Boardwalk,Cherry Hill, NJ
|
||||||
|
218,Laughing Dog Brewing Company,Ponderay, ID
|
||||||
|
219,Bozeman Brewing Company,Bozeman, MT
|
||||||
|
220,Big Choice Brewing,Broomfield, CO
|
||||||
|
221,Big Storm Brewing Company,Odessa, FL
|
||||||
|
222,Carton Brewing Company,Atlantic Highlands, NJ
|
||||||
|
223,Midnight Sun Brewing Company,Anchorage, AK
|
||||||
|
224,Fat Head's Brewery,Middleburg Heights, OH
|
||||||
|
225,Refuge Brewery,Temecula, CA
|
||||||
|
226,Chatham Brewing,Chatham, NY
|
||||||
|
227,DC Brau Brewing Company,Washington, DC
|
||||||
|
228,Geneva Lake Brewing Company,Lake Geneva, WI
|
||||||
|
229,Rochester Mills Brewing Company,Rochester, MI
|
||||||
|
230,Cape Ann Brewing Company,Gloucester, MA
|
||||||
|
231,Borderlands Brewing Company,Tucson, AZ
|
||||||
|
232,College Street Brewhouse and Pub,Lake Havasu City, AZ
|
||||||
|
233,Joseph James Brewing Company,Henderson, NV
|
||||||
|
234,Harpoon Brewery,Boston, MA
|
||||||
|
235,Back East Brewing Company,Bloomfield, CT
|
||||||
|
236,Champion Brewing Company,Charlottesville, VA
|
||||||
|
237,Devil's Backbone Brewing Company,Lexington, VA
|
||||||
|
238,Newburgh Brewing Company,Newburgh, NY
|
||||||
|
239,Wiseacre Brewing Company,Memphis, TN
|
||||||
|
240,Golden Road Brewing,Los Angeles, CA
|
||||||
|
241,New Republic Brewing Company,College Station, TX
|
||||||
|
242,Infamous Brewing Company,Austin, TX
|
||||||
|
243,Two Henrys Brewing Company,Plant City, FL
|
||||||
|
244,Lift Bridge Brewing Company,Stillwater, MN
|
||||||
|
245,Lucky Town Brewing Company,Jackson, MS
|
||||||
|
246,Quest Brewing Company,Greenville, SC
|
||||||
|
247,Creature Comforts,Athens, GA
|
||||||
|
248,Half Full Brewery,Stamford, CT
|
||||||
|
249,Southampton Publick House,Southampton, NY
|
||||||
|
250,Chapman's Brewing,Angola, IN
|
||||||
|
251,Barrio Brewing Company,Tucson, AZ
|
||||||
|
252,Santa Cruz Mountain Brewing,Santa Cruz, CA
|
||||||
|
253,Frankenmuth Brewery,Frankenmuth, MI
|
||||||
|
254,Meckley's Cidery,Somerset Center, MI
|
||||||
|
255,Stillwater Artisanal Ales,Baltimore, MD
|
||||||
|
256,Finch's Beer Company,Chicago, IL
|
||||||
|
257,South Austin Brewery,South Austin, TX
|
||||||
|
258,Bauhaus Brew Labs,Minneapolis, MN
|
||||||
|
259,Ozark Beer Company,Rogers, AR
|
||||||
|
260,Mountain Town Brewing Company ,Mount Pleasant, MI
|
||||||
|
261,Otter Creek Brewing,Waterbury, VT
|
||||||
|
262,The Brewer's Art,Baltimore, MD
|
||||||
|
263,Denver Beer Company,Denver, CO
|
||||||
|
264,Ska Brewing Company,Durango, CO
|
||||||
|
265,Tractor Brewing Company,Albuquerque, NM
|
||||||
|
266,Peak Organic Brewing Company,Portland, ME
|
||||||
|
267,Cape Cod Beer,Hyannis, MA
|
||||||
|
268,Long Trail Brewing Company,Bridgewater Corners, VT
|
||||||
|
269,Great Raft Brewing Company,Shreveport, LA
|
||||||
|
270,Alaskan Brewing Company,Juneau, AK
|
||||||
|
271,Notch Brewing Company,Ipswich, MA
|
||||||
|
272,The Alchemist,Waterbury, VT
|
||||||
|
273,Three Notch'd Brewing Company,Charlottesville, VA
|
||||||
|
274,Portside Brewery,Cleveland, OH
|
||||||
|
275,Otter Creek Brewing,Middlebury, VT
|
||||||
|
276,Montauk Brewing Company,Montauk, NY
|
||||||
|
277,Indeed Brewing Company,Minneapolis, MN
|
||||||
|
278,Berkshire Brewing Company,South Deerfield, MA
|
||||||
|
279,Foolproof Brewing Company,Pawtucket, RI
|
||||||
|
280,Headlands Brewing Company,Mill Valley, CA
|
||||||
|
281,Bolero Snort Brewery,Ridgefield Park, NJ
|
||||||
|
282,Thunderhead Brewing Company,Kearney, NE
|
||||||
|
283,Defiance Brewing Company,Hays, KS
|
||||||
|
284,Milwaukee Brewing Company,Milwaukee, WI
|
||||||
|
285,Catawba Island Brewing,Port Clinton, OH
|
||||||
|
286,Back Forty Beer Company,Gadsden, AL
|
||||||
|
287,Four Corners Brewing Company,Dallas, TX
|
||||||
|
288,Saint Archer Brewery,San Diego, CA
|
||||||
|
289,Rogue Ales,Newport, OR
|
||||||
|
290,Hale's Ales,Seattle, WA
|
||||||
|
291,Tommyknocker Brewery,Idaho Springs, CO
|
||||||
|
292,Baxter Brewing Company,Lewiston, ME
|
||||||
|
293,Northampton Brewery,Northamtpon, MA
|
||||||
|
294,Black Shirt Brewing Company,Denver, CO
|
||||||
|
295,Wachusett Brewing Company,Westminster, MA
|
||||||
|
296,Widmer Brothers Brewing Company,Portland, OR
|
||||||
|
297,Hop Farm Brewing Company,Pittsburgh, PA
|
||||||
|
298,Liquid Hero Brewery,York, PA
|
||||||
|
299,Matt Brewing Company,Utica, NY
|
||||||
|
300,Boston Beer Company,Boston, MA
|
||||||
|
301,Old Forge Brewing Company,Danville, PA
|
||||||
|
302,Utah Brewers Cooperative,Salt Lake City, UT
|
||||||
|
303,Magic Hat Brewing Company,South Burlington, VT
|
||||||
|
304,Blue Hills Brewery,Canton, MA
|
||||||
|
305,Night Shift Brewing,Everett, MA
|
||||||
|
306,Beach Brewing Company,Virginia Beach, VA
|
||||||
|
307,Payette Brewing Company,Garden City, ID
|
||||||
|
308,Brew Bus Brewing,Tampa, FL
|
||||||
|
309,Sockeye Brewing Company,Boise, ID
|
||||||
|
310,Pine Street Brewery,San Francisco, CA
|
||||||
|
311,Dirty Bucket Brewing Company,Woodinville, WA
|
||||||
|
312,Jackalope Brewing Company,Nashville, TN
|
||||||
|
313,Slanted Rock Brewing Company,Meridian, ID
|
||||||
|
314,Piney River Brewing Company,Bucryus, MO
|
||||||
|
315,Cutters Brewing Company,Avon, IN
|
||||||
|
316,Iron Hill Brewery & Restaurant,Wilmington, DE
|
||||||
|
317,Marshall Wharf Brewing Company,Belfast, ME
|
||||||
|
318,Banner Beer Company,Williamsburg, MA
|
||||||
|
319,Dick's Brewing Company,Centralia, WA
|
||||||
|
320,Claremont Craft Ales,Claremont, CA
|
||||||
|
321,Rivertown Brewing Company,Lockland, OH
|
||||||
|
322,Voodoo Brewery,Meadville, PA
|
||||||
|
323,D.L. Geary Brewing Company,Portland, ME
|
||||||
|
324,Pisgah Brewing Company,Black Mountain, NC
|
||||||
|
325,Neshaminy Creek Brewing Company,Croydon, PA
|
||||||
|
326,Morgan Street Brewery,Saint Louis, MO
|
||||||
|
327,Half Acre Beer Company,Chicago, IL
|
||||||
|
328,The Just Beer Project,Burlington, VT
|
||||||
|
329,The Bronx Brewery,Bronx, NY
|
||||||
|
330,Dead Armadillo Craft Brewing,Tulsa, OK
|
||||||
|
331,Catawba Brewing Company,Morganton, NC
|
||||||
|
332,La Cumbre Brewing Company,Albuquerque, NM
|
||||||
|
333,David's Ale Works,Diamond Springs, CA
|
||||||
|
334,The Traveler Beer Company,Burlington, VT
|
||||||
|
335,Fargo Brewing Company,Fargo, ND
|
||||||
|
336,Big Sky Brewing Company,Missoula, MT
|
||||||
|
337,Nebraska Brewing Company,Papillion, NE
|
||||||
|
338,Uncle John's Fruit House Winery,St. John's, MI
|
||||||
|
339,Wormtown Brewery,Worcester, MA
|
||||||
|
340,Due South Brewing Company,Boynton Beach, FL
|
||||||
|
341,Palisade Brewing Company,Palisade, CO
|
||||||
|
342,KelSo Beer Company,Brooklyn, NY
|
||||||
|
343,Hardywood Park Craft Brewery,Richmond, VA
|
||||||
|
344,Wolf Hills Brewing Company,Abingdon, VA
|
||||||
|
345,Lavery Brewing Company,Erie, PA
|
||||||
|
346,Manzanita Brewing Company,Santee, CA
|
||||||
|
347,Fullsteam Brewery,Durham, NC
|
||||||
|
348,Four Horsemen Brewing Company,South Bend, IN
|
||||||
|
349,Hinterland Brewery,Green Bay, WI
|
||||||
|
350,Central Coast Brewing Company,San Luis Obispo, CA
|
||||||
|
351,Westfield River Brewing Company,Westfield, MA
|
||||||
|
352,Elevator Brewing Company,Columbus, OH
|
||||||
|
353,Aslan Brewing Company,Bellingham, WA
|
||||||
|
354,Kulshan Brewery,Bellingham, WA
|
||||||
|
355,Pikes Peak Brewing Company,Monument, CO
|
||||||
|
356,Manayunk Brewing Company,Philadelphia, PA
|
||||||
|
357,Buckeye Brewing,Cleveland, OH
|
||||||
|
358,Daredevil Brewing Company,Shelbyville, IN
|
||||||
|
359,NoDa Brewing Company,Charlotte, NC
|
||||||
|
360,Aviator Brewing Company,Fuquay-Varina, NC
|
||||||
|
361,Wild Onion Brewing Company,Lake Barrington, IL
|
||||||
|
362,Hilliard's Beer,Seattle, WA
|
||||||
|
363,Mikkeller,Pottstown, PA
|
||||||
|
364,Bohemian Brewery,Midvale, UT
|
||||||
|
365,Great River Brewery,Davenport, IA
|
||||||
|
366,Mustang Brewing Company,Mustang, OK
|
||||||
|
367,Airways Brewing Company,Kent, WA
|
||||||
|
368,21st Amendment Brewery,San Francisco, CA
|
||||||
|
369,Eddyline Brewery & Restaurant,Buena Vista, CO
|
||||||
|
370,Pizza Port Brewing Company,Carlsbad, CA
|
||||||
|
371,Sly Fox Brewing Company,Pottstown, PA
|
||||||
|
372,Spring House Brewing Company,Conestoga, PA
|
||||||
|
373,7venth Sun,Dunedin, FL
|
||||||
|
374,Astoria Brewing Company,Astoria, OR
|
||||||
|
375,Maui Brewing Company,Lahaina, HI
|
||||||
|
376,RoughTail Brewing Company,Midwest City, OK
|
||||||
|
377,Lucette Brewing Company,Menominee, WI
|
||||||
|
378,Bold City Brewery,Jacksonville, FL
|
||||||
|
379,Grey Sail Brewing of Rhode Island,Westerly, RI
|
||||||
|
380,Blue Blood Brewing Company,Lincoln, NE
|
||||||
|
381,Swashbuckler Brewing Company,Manheim, PA
|
||||||
|
382,Blue Mountain Brewery,Afton, VA
|
||||||
|
383,Starr Hill Brewery,Crozet, VA
|
||||||
|
384,Westbrook Brewing Company,Mt. Pleasant, SC
|
||||||
|
385,Shipyard Brewing Company,Portland, ME
|
||||||
|
386,Revolution Brewing,Paonia, CO
|
||||||
|
387,Natian Brewery,Portland, OR
|
||||||
|
388,Alltech's Lexington Brewing Company,Lexington, KY
|
||||||
|
389,Oskar Blues Brewery (North Carol...,Brevard, NC
|
||||||
|
390,Orlison Brewing Company,Airway Heights, WA
|
||||||
|
391,Breckenridge Brewery,Denver, CO
|
||||||
|
392,Santa Fe Brewing Company,Santa Fe, NM
|
||||||
|
393,Miami Brewing Company,Miami, FL
|
||||||
|
394,Schilling & Company,Seattle, WA
|
||||||
|
395,Hops & Grain Brewery,Austin, TX
|
||||||
|
396,White Flame Brewing Company,Hudsonville, MI
|
||||||
|
397,Ruhstaller Beer Company,Sacramento, CA
|
||||||
|
398,Saugatuck Brewing Company,Douglas, MI
|
||||||
|
399,Moab Brewery,Moab, UT
|
||||||
|
400,Macon Beer Company,Macon, GA
|
||||||
|
401,Amnesia Brewing Company,Washougal, WA
|
||||||
|
402,Wolverine State Brewing Company,Ann Arbor, MI
|
||||||
|
403,Red Tank Cider Company,Bend, OR
|
||||||
|
404,Cascadia Ciderworks United,Portland, OR
|
||||||
|
405,Fate Brewing Company,Boulder, CO
|
||||||
|
406,Lazy Monk Brewing,Eau Claire, WI
|
||||||
|
407,Bitter Root Brewing,Hamilton, MT
|
||||||
|
408,10 Barrel Brewing Company,Bend, OR
|
||||||
|
409,Tamarack Brewing Company,Lakeside, MT
|
||||||
|
410,New England Brewing Company,Woodbridge, CT
|
||||||
|
411,Seattle Cider Company,Seattle, WA
|
||||||
|
412,Straight to Ale,Huntsville, AL
|
||||||
|
413,Austin Beerworks,Austin, TX
|
||||||
|
414,Blue Mountain Brewery,Arrington, VA
|
||||||
|
415,Coastal Empire Beer Company,Savannah, GA
|
||||||
|
416,Jack's Hard Cider (Hauser Estate...,Biglerville, PA
|
||||||
|
417,Boulder Beer Company,Boulder, CO
|
||||||
|
418,Coalition Brewing Company,Portland, OR
|
||||||
|
419,Sanitas Brewing Company,Boulder, CO
|
||||||
|
420,Gore Range Brewery,Edwards, CO
|
||||||
|
421,Redstone Meadery,Boulder, CO
|
||||||
|
422,Blue Dog Mead,Eugene, OR
|
||||||
|
423,Hess Brewing Company,San Diego, CA
|
||||||
|
424,Wynkoop Brewing Company,Denver, CO
|
||||||
|
425,Ciderboys,Stevens Point, WI
|
||||||
|
426,Armadillo Ale Works,Denton, TX
|
||||||
|
427,Roanoke Railhouse Brewery,Roanoke, VA
|
||||||
|
428,Schlafly Brewing Company,Saint Louis, MO
|
||||||
|
429,Asher Brewing Company,Boulder, CO
|
||||||
|
430,Lost Rhino Brewing Company,Ashburn, VA
|
||||||
|
431,North Country Brewing Company,Slippery Rock, PA
|
||||||
|
432,Seabright Brewery,Santa Cruz, CA
|
||||||
|
433,French Broad Brewery,Asheville, NC
|
||||||
|
434,Angry Orchard Cider Company,Cincinnati, OH
|
||||||
|
435,Two Roads Brewing Company,Stratford, CT
|
||||||
|
436,Southern Oregon Brewing Company,Medford, OR
|
||||||
|
437,Brooklyn Brewery,Brooklyn, NY
|
||||||
|
438,The Right Brain Brewery,Traverse City, MI
|
||||||
|
439,Kona Brewing Company,Kona, HI
|
||||||
|
440,MillKing It Productions,Royal Oak, MI
|
||||||
|
441,Pateros Creek Brewing Company,Fort Collins, CO
|
||||||
|
442,O'Fallon Brewery,O'Fallon, MO
|
||||||
|
443,Marble Brewery,Albuquerque, NM
|
||||||
|
444,Big Wood Brewery,Vadnais Heights, MN
|
||||||
|
445,Howard Brewing Company,Lenoir, NC
|
||||||
|
446,Downeast Cider House,Leominster, MA
|
||||||
|
447,Swamp Head Brewery,Gainesville, FL
|
||||||
|
448,Mavericks Beer Company,Half Moon Bay, CA
|
||||||
|
449,TailGate Beer,San Diego, CA
|
||||||
|
450,Northwest Brewing Company,Pacific, WA
|
||||||
|
451,Dad & Dude's Breweria,Aurora, CO
|
||||||
|
452,Centennial Beer Company,Edwards, CO
|
||||||
|
453,Denali Brewing Company,Talkeetna, AK
|
||||||
|
454,Deschutes Brewery,Bend, OR
|
||||||
|
455,Sunken City Brewing Company,Hardy, VA
|
||||||
|
456,Lucette Brewing Company,Menominie, WI
|
||||||
|
457,The Black Tooth Brewing Company,Sheridan, WY
|
||||||
|
458,Kenai River Brewing Company,Soldotna, AK
|
||||||
|
459,River North Brewery,Denver, CO
|
||||||
|
460,Fremont Brewing Company,Seattle, WA
|
||||||
|
461,Armstrong Brewing Company,South San Francisco, CA
|
||||||
|
462,AC Golden Brewing Company,Golden, CO
|
||||||
|
463,Big Bend Brewing Company,Alpine, TX
|
||||||
|
464,Good Life Brewing Company,Bend, OR
|
||||||
|
465,Engine 15 Brewing,Jacksonville Beach, FL
|
||||||
|
466,Green Room Brewing,Jacksonville, FL
|
||||||
|
467,Brindle Dog Brewing Company,Tampa Bay, FL
|
||||||
|
468,Peace Tree Brewing Company,Knoxville, IA
|
||||||
|
469,Terrapin Brewing Company,Athens, GA
|
||||||
|
470,Pete's Brewing Company,San Antonio, TX
|
||||||
|
471,Okoboji Brewing Company,Spirit Lake, IA
|
||||||
|
472,Crystal Springs Brewing Company,Boulder, CO
|
||||||
|
473,Engine House 9,Tacoma, WA
|
||||||
|
474,Tonka Beer Company,Minnetonka, MN
|
||||||
|
475,Red Hare Brewing Company,Marietta, GA
|
||||||
|
476,Hangar 24 Craft Brewery,Redlands, CA
|
||||||
|
477,Big Elm Brewing,Sheffield, MA
|
||||||
|
478,Good People Brewing Company,Birmingham, AL
|
||||||
|
479,Heavy Seas Beer,Halethorpe, MD
|
||||||
|
480,Telluride Brewing Company,Telluride, CO
|
||||||
|
481,7 Seas Brewing Company,Gig Harbor, WA
|
||||||
|
482,Confluence Brewing Company,Des Moines, IA
|
||||||
|
483,Bale Breaker Brewing Company,Yakima, WA
|
||||||
|
484,The Manhattan Brewing Company,New York, NY
|
||||||
|
485,MacTarnahans Brewing Company,Portland, OR
|
||||||
|
486,Stillmank Beer Company,Green Bay, WI
|
||||||
|
487,Redhook Brewery,Woodinville, WA
|
||||||
|
488,Dock Street Brewery,Philadelphia, PA
|
||||||
|
489,Blue Point Brewing Company,Patchogue, NY
|
||||||
|
490,Tampa Bay Brewing Company,Tampa, FL
|
||||||
|
491,Devil's Canyon Brewery,Belmont, CA
|
||||||
|
492,Stone Coast Brewing Company,Portland, ME
|
||||||
|
493,Broken Tooth Brewing Company,Anchorage, AK
|
||||||
|
494,Seven Brides Brewery,Silverton, OR
|
||||||
|
495,Newburyport Brewing Company,Newburyport, MA
|
||||||
|
496,Dry Dock Brewing Company,Aurora, CO
|
||||||
|
497,Cans Bar and Canteen,Charlotte, NC
|
||||||
|
498,Sprecher Brewing Company,Glendale, WI
|
||||||
|
499,Wildwood Brewing Company,Stevensville, MT
|
||||||
|
500,High Noon Saloon And Brewery,Leavenworth, KS
|
||||||
|
501,Woodchuck Hard Cider,Middlebury, VT
|
||||||
|
502,Sea Dog Brewing Company,Portland, ME
|
||||||
|
503,Oskar Blues Brewery,Lyons, CO
|
||||||
|
504,Carolina Beer & Beverage,Mooresville, NC
|
||||||
|
505,Krebs Brewing Company (Pete's Pl...,Krebs, OK
|
||||||
|
506,Warbird Brewing Company,Fort Wayne, IN
|
||||||
|
507,Mudshark Brewing Company,Lake Havasu City, AZ
|
||||||
|
508,Spilker Ales,Cortland, NE
|
||||||
|
509,Wingman Brewers,Tacoma, WA
|
||||||
|
510,Kettle House Brewing Company,Missoula, MT
|
||||||
|
511,Sherwood Forest Brewers,Marlborough, MA
|
||||||
|
512,Cottrell Brewing,Pawcatuck, CT
|
||||||
|
513,Arctic Craft Brewery,Colorado Springs, CO
|
||||||
|
514,Monkey Paw Pub & Brewery,San Diego, CA
|
||||||
|
515,Crabtree Brewing Company,Greeley, CO
|
||||||
|
516,Emerald City Beer Company,Seattle, WA
|
||||||
|
517,Butcher's Brewing,Carlsbad, CA
|
||||||
|
518,New South Brewing Company,Myrtle Beach, SC
|
||||||
|
519,Big River Brewing Company,Chattanooga, TN
|
||||||
|
520,Twisted Pine Brewing Company,Boulder, CO
|
||||||
|
521,Flying Dog Brewery,Frederick, MD
|
||||||
|
522,Uncommon Brewers,Santa Cruz, CA
|
||||||
|
523,Aspen Brewing Company,Aspen, CO
|
||||||
|
524,Triangle Brewing Company,Durham, NC
|
||||||
|
525,Bomb Beer Company,New York, NY
|
||||||
|
526,Churchkey Can Company,Seattle, WA
|
||||||
|
527,Intuition Ale Works,Jacksonville, FL
|
||||||
|
528,Asheville Brewing Company,Asheville, NC
|
||||||
|
529,Northwoods Brewpub,Eau Claire, WI
|
||||||
|
530,Buckbean Brewing Company,Reno, NV
|
||||||
|
531,Dolores River Brewery,Dolores, CO
|
||||||
|
532,Flat Rock Brewing Company,Smithton, PA
|
||||||
|
533,Abita Brewing Company,Abita Springs, LA
|
||||||
|
534,Mammoth Brewing Company,Mammoth Lakes, CA
|
||||||
|
535,Harvest Moon Brewing Company,Belt, MT
|
||||||
|
536,Grand Canyon Brewing Company,Williams, AZ
|
||||||
|
537,Lewis and Clark Brewing Company,Helena, MT
|
||||||
|
538,Dundee Brewing Company,Rochester, NY
|
||||||
|
539,Twin Lakes Brewing Company,Greenville, DE
|
||||||
|
540,Mother Earth Brewing Company,Kinston, NC
|
||||||
|
541,Arcadia Brewing Company,Battle Creek, MI
|
||||||
|
542,Angry Minnow Brewing Company,Hayward, WI
|
||||||
|
543,Great Northern Brewing Company,Whitefish, MT
|
||||||
|
544,Pyramid Breweries,Seattle, WA
|
||||||
|
545,Lancaster Brewing Company,Lancaster, PA
|
||||||
|
546,Upstate Brewing Company,Elmira, NY
|
||||||
|
547,Moat Mountain Smoke House & Brew...,North Conway, NH
|
||||||
|
548,Prescott Brewing Company,Prescott, AZ
|
||||||
|
549,Mogollon Brewing Company,Flagstaff, AZ
|
||||||
|
550,Wind River Brewing Company,Pinedale, WY
|
||||||
|
551,Silverton Brewery,Silverton, CO
|
||||||
|
552,Mickey Finn's Brewery,Libertyville, IL
|
||||||
|
553,Covington Brewhouse,Covington, LA
|
||||||
|
554,Dave's Brewfarm,Wilson, WI
|
||||||
|
555,Ukiah Brewing Company,Ukiah, CA
|
||||||
|
556,Butternuts Beer and Ale,Garrattsville, NY
|
||||||
|
557,Sleeping Lady Brewing Company,Anchorage, AK
|
||||||
|
162686
misc/raw-data/breweries.json
Normal file
162686
misc/raw-data/breweries.json
Normal file
File diff suppressed because it is too large
Load Diff
578
misc/raw-data/ontariobreweries.json
Normal file
578
misc/raw-data/ontariobreweries.json
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"text": "100 Acre Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/100-acre-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "All My Friends Beer Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/all-my-friends-beer-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "All or Nothing Brewhouse",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/all-or-nothing-brewhouse/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Anderson Craft Ales",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/anderson-craft-ales/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Badlands Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/badlands-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Bancroft Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/bancroft-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Banded Goose Brewing Comany",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/banded-goose-brewing-comany/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Beau’s Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/beaus-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "BeerLab! London",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/beerlab-london/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Bellwoods Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/bellwoods-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Bench Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/bench-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Beyond The Pale Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/beyond-the-pale-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Bicycle Craft Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/bicycle-craft-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Big Rig Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/big-rig-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Big Rock Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/big-rock-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Black Gold Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/black-gold-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Black Oak Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/black-oak-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Block 3 Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/block-3-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Blood Brothers Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/blood-brothers-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Bobcaygeon Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/bobcaygeon-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Boshkung Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/boshkung-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Brauwerk Hoffman – Rockland",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/brauwerk-hoffman-rockland/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Bridge Masters Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/bridge-masters-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Broadhead Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/broadhead-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Broken Rail Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/broken-rail-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Burdock Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/burdock-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "C’est What Durham Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/cest-what-durham-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Calabogie Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/calabogie-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Cameron’s Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/camerons-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Canvas Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/canvas-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Caps Off Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/caps-off-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Century Barn Brewing & Beverage Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/century-barn-brewing-and-beverage-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Chronicle Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/chronicle-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Clifford Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/clifford-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Cold Bear Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/cold-bear-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Collective Arts Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/collective-arts-brewing-ltd/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Common Good Beer Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/common-good-beer-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Couchiching Craft Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/couchiching-craft-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Cowbell Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/cowbell-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Cured Craft Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/cured-craft-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Daft Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/daft-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Dog House Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/dog-house-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Dominion City Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/dominion-city-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Eastbound Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/eastbound-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Equals Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/equals-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Fairweather Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/fairweather-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Farm League Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/farm-league-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Fixed Gear Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/fixed-gear-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Flying Monkeys Craft Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/flying-monkeys-craft-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Focal Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/focal-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Foundry Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/foundry-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Four Fathers Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/four-fathers-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Frank Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/frank-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Freddy’s",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/freddys/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Full Beard Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/1068-2/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Furnace Room Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/furnace-room-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Gateway City Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/gateway-city-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Glasstown Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/glasstown-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Godspeed Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/godspeed-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Goldenfield Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/goldenfield-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Grand River Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/grand-river-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Granite Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/granite-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Great Lakes Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/great-lakes-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Haliburton Highlands Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/haliburton-highlands-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Imperial City Brew House",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/imperial-city-brew-house/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Indie Ale House",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/indie-ale-house/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Jobsite Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/jobsite-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Kichesippi Beer Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/kichesippi-beer-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Kick and Push Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/kick-and-push-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Lake of Bays Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/lake-of-bays-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Lake Of The Woods Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/lake-of-the-woods-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Left Field Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/left-field-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Lightcaster Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/lightcaster-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "MacKinnon Brothers Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/mackinnon-brothers-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Maclean’s Ales Inc.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/macleans-ales-inc/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Magnotta Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/magnotta-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Market Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/market-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Mascot Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/mascot-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Matron Fine Beer",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/matron-fine-beer/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Meyers Creek Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/meyers-creek-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Midtown Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/midtown-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Miski Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/miski-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Muddy York Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/muddy-york-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Muskoka Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/muskoka-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Natterjack Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/natterjack-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Newark Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/newark-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Niagara Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/niagara-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Niagara College Teaching Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/niagara-college-teaching-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Niagara Oast House Brewers",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/niagara-oast-house-brewers/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Nickel Brook Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/nickel-brook-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Northern Superior Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/northern-superior-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Old Credit Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/old-credit-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Old Flame Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/1239-2/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Orléans Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/orleans-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Overflow Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/overflow-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Parsons Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/parsons-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Perth Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/perth-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Prince Eddy’s Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/prince-eddys-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Quayle’s Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/quayles-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Quetico Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/quetico-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Railway City Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/railway-city-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Ramblin’ Road Brewery Farm",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/ramblin-road-brewery-farm/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Red Barn Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/red-barn-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Refined Fool Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/refined-fool-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Rouge River Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/rouge-river-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Royal City Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/royal-city-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Sassy Britches Brewing Co Ltd",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/sassy-britches-brewing-co-ltd/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Sawdust City Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/sawdust-city-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Shawn & Ed Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/shawn-ed-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Silversmith Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/silversmith-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Slake Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/slake-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Sleeping Giant Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/sleeping-giant-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Something in the Water Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/something-in-the-water-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Sonnen Hill Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/sonnen-hill-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Sons of Kent Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/sons-of-kent-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Spark Beer",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/spark-beer/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Split Rail Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/split-rail-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Stack Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/stack-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Steam Whistle Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/steam-whistle-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Steel Wheel Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/steel-wheel-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Stonehooker Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/stonehooker-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Stonepicker Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/stonepicker-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Stray Dog Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/stray-dog-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "The Exchange Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/the-exchange-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "The Grove Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/the-grove-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "The Second Wedge Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/the-second-wedge-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Thornbury Village Craft Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/thornbury-village-craft-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Three Sheets Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/three-sheets-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Tooth and Nail Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/tooth-and-nail-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Torched Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/torched-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Town Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/town-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Trestle Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/trestle-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "True History Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/true-history-brewing/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Upper Thames Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/upper-thames-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Vimy Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/vimy-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Walkerville Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/walkerville-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Wave Maker Craft Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/wave-maker-craft-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Wellington Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/wellington-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Whiprsnapr Brewing Co.",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/whiprsnapr-brewing-co/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Whiskeyjack Beer Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/whiskeyjack-beer-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Whitewater Brewing Company",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/whitewater-brewing-company/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Willibald Farm Distillery & Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/willibald-farm-distillery-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Windmill Brewery",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/windmill-brewery/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Wishbone Brewing",
|
||||||
|
"href": "https://ontariocraftbrewers.com/brewery-profile/wishbone-brewing/"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -6,4 +6,3 @@ data
|
|||||||
models
|
models
|
||||||
*.gguf
|
*.gguf
|
||||||
BiergartenPipeline.png
|
BiergartenPipeline.png
|
||||||
output
|
|
||||||
181
pipeline/CMakeLists.txt
Normal file
181
pipeline/CMakeLists.txt
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
project(biergarten-pipeline)
|
||||||
|
|
||||||
|
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 1. Platform & GPU Detection
|
||||||
|
# =============================================================================
|
||||||
|
if(WIN32)
|
||||||
|
message(FATAL_ERROR "[biergarten] Windows is currently not supported. Please use Linux (Fedora 43) or macOS (M1 Pro).")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64")
|
||||||
|
message(STATUS "[biergarten] Apple Silicon detected — enabling Metal acceleration.")
|
||||||
|
set(GGML_METAL ON CACHE BOOL "Enable Metal for Apple Silicon" FORCE)
|
||||||
|
else()
|
||||||
|
message(STATUS "[biergarten] Intel Mac detected — using CPU / Accelerate framework.")
|
||||||
|
set(GGML_METAL OFF CACHE BOOL "Disable Metal for Intel Macs" FORCE)
|
||||||
|
endif()
|
||||||
|
elseif(UNIX AND NOT APPLE)
|
||||||
|
find_package(CUDAToolkit QUIET)
|
||||||
|
find_package(HIP QUIET)
|
||||||
|
|
||||||
|
if(CUDAToolkit_FOUND)
|
||||||
|
message(STATUS "[biergarten] NVIDIA GPU detected — enabling CUDA acceleration.")
|
||||||
|
set(GGML_CUDA ON CACHE BOOL "Enable CUDA for NVIDIA GPUs" FORCE)
|
||||||
|
set(CMAKE_CUDA_ARCHITECTURES native)
|
||||||
|
elseif(HIP_FOUND OR EXISTS "/opt/rocm")
|
||||||
|
message(STATUS "[biergarten] AMD GPU detected — enabling HIP/ROCm acceleration.")
|
||||||
|
set(GGML_HIPBLAS ON CACHE BOOL "Enable HIP for AMD GPUs" FORCE)
|
||||||
|
else()
|
||||||
|
message(STATUS "[biergarten] No NVIDIA or AMD GPU found — falling back to CPU.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 2. Project-wide Settings (Standard & Optimization)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# Release Build Optimization: Aggressive (-O3), Arch-specific, and LTO
|
||||||
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=native -flto")
|
||||||
|
|
||||||
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og -g")
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 3. Dependencies
|
||||||
|
# =============================================================================
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
find_package(CURL QUIET)
|
||||||
|
if(NOT CURL_FOUND)
|
||||||
|
message(FATAL_ERROR "[biergarten] libcurl not found. Install it (e.g. 'sudo dnf install libcurl-devel').")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Require system Boost for JSON and Program Options to speed up build times
|
||||||
|
find_package(Boost REQUIRED COMPONENTS json program_options)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
sqlite_amalgamation
|
||||||
|
URL https://www.sqlite.org/2026/sqlite-amalgamation-3530000.zip
|
||||||
|
URL_HASH SHA3_256=c2325c53b3b41761469f91cfb078e96882ac5d85bac10c11b0bd8f253b031e5b
|
||||||
|
)
|
||||||
|
FetchContent_GetProperties(sqlite_amalgamation)
|
||||||
|
if(NOT sqlite_amalgamation_POPULATED)
|
||||||
|
FetchContent_Populate(sqlite_amalgamation)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT TARGET sqlite3)
|
||||||
|
add_library(sqlite3 STATIC
|
||||||
|
${sqlite_amalgamation_SOURCE_DIR}/sqlite3.c
|
||||||
|
)
|
||||||
|
target_include_directories(sqlite3 PUBLIC
|
||||||
|
${sqlite_amalgamation_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
target_compile_definitions(sqlite3 PUBLIC
|
||||||
|
SQLITE_THREADSAFE=1
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
llama-cpp
|
||||||
|
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp.git
|
||||||
|
GIT_TAG b8742
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(llama-cpp)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
boost-di
|
||||||
|
GIT_REPOSITORY https://github.com/boost-ext/di.git
|
||||||
|
GIT_TAG v1.3.0
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(boost-di)
|
||||||
|
if(TARGET Boost.DI AND NOT TARGET boost::di)
|
||||||
|
add_library(boost::di ALIAS Boost.DI)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
spdlog
|
||||||
|
GIT_REPOSITORY https://github.com/gabime/spdlog.git
|
||||||
|
GIT_TAG v1.15.3
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(spdlog)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 4. Sources
|
||||||
|
# =============================================================================
|
||||||
|
set(SOURCES
|
||||||
|
src/main.cc
|
||||||
|
src/biergarten_data_generator/biergarten_data_generator.cc
|
||||||
|
src/biergarten_data_generator/run.cc
|
||||||
|
src/biergarten_data_generator/query_cities_with_countries.cc
|
||||||
|
src/biergarten_data_generator/generate_breweries.cc
|
||||||
|
src/biergarten_data_generator/log_results.cc
|
||||||
|
src/services/wikipedia/wikipedia_service.cc
|
||||||
|
src/services/wikipedia/get_summary.cc
|
||||||
|
src/services/wikipedia/fetch_extract.cc
|
||||||
|
src/services/sqlite/sqlite_export_service.cc
|
||||||
|
src/services/sqlite/build_database_path.cc
|
||||||
|
src/services/sqlite/build_location_key.cc
|
||||||
|
src/services/sqlite/initialize_schema.cc
|
||||||
|
src/services/sqlite/prepare_statements.cc
|
||||||
|
src/services/sqlite/initialize.cc
|
||||||
|
src/services/sqlite/process_record.cc
|
||||||
|
src/services/sqlite/finalize_statements.cc
|
||||||
|
src/services/sqlite/rollback_and_close_no_throw.cc
|
||||||
|
src/services/sqlite/finalize.cc
|
||||||
|
src/web_client/curl_global_state.cc
|
||||||
|
src/web_client/curl_web_client_get.cc
|
||||||
|
src/web_client/curl_web_client_url_encode.cc
|
||||||
|
src/data_generation/llama/llama_generator.cc
|
||||||
|
src/data_generation/llama/generate_brewery.cc
|
||||||
|
src/data_generation/llama/generate_user.cc
|
||||||
|
src/data_generation/llama/helpers.cc
|
||||||
|
src/data_generation/llama/infer.cc
|
||||||
|
src/data_generation/llama/load.cc
|
||||||
|
src/data_generation/llama/load_brewery_prompt.cc
|
||||||
|
src/data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.cc
|
||||||
|
src/data_generation/mock/deterministic_hash.cc
|
||||||
|
src/data_generation/mock/generate_brewery.cc
|
||||||
|
src/data_generation/mock/generate_user.cc
|
||||||
|
src/json_handling/json_loader.cc
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 5. Target
|
||||||
|
# =============================================================================
|
||||||
|
add_executable(${PROJECT_NAME} ${SOURCES})
|
||||||
|
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||||
|
includes
|
||||||
|
${llama-cpp_SOURCE_DIR}/include
|
||||||
|
${llama-cpp_SOURCE_DIR}/common
|
||||||
|
)
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||||
|
llama
|
||||||
|
boost::di
|
||||||
|
Boost::json
|
||||||
|
Boost::program_options
|
||||||
|
spdlog::spdlog
|
||||||
|
sqlite3
|
||||||
|
CURL::libcurl
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 6. Runtime Assets
|
||||||
|
# =============================================================================
|
||||||
|
configure_file(
|
||||||
|
${CMAKE_SOURCE_DIR}/locations.json
|
||||||
|
${CMAKE_BINARY_DIR}/locations.json
|
||||||
|
COPYONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
${CMAKE_SOURCE_DIR}/prompts
|
||||||
|
${CMAKE_BINARY_DIR}/prompts
|
||||||
|
)
|
||||||
@@ -44,20 +44,20 @@ distributions, rating skews, activity profiles) will be handled the same way.
|
|||||||
as a source of truth for brewing techniques or cultural representation.**
|
as a source of truth for brewing techniques or cultural representation.**
|
||||||
|
|
||||||
**Natural language processing, although a powerful tool for data analysis and
|
**Natural language processing, although a powerful tool for data analysis and
|
||||||
generation is to be taken with scrutiny. Human language is not simply just data
|
generation is to be taken with scrutiny. Human language is not simply data
|
||||||
points to be analyzed, but it also carries deep cultural and human meaning that
|
points to be analyzed, but carries deep cultural and human meaning that
|
||||||
artificial intelligence is incapable of.**
|
artificial intelligence is incapable of.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Model Bias and Language Quality
|
## Model Bias and Language Quality
|
||||||
|
|
||||||
The underlying model's training biases surface within this pipeline. Output
|
The underlying model's training biases surface measurably in this pipeline.
|
||||||
quality tracks with how well a language is represented in the training corpus:
|
Output quality tracks with how well a language is represented in the training
|
||||||
standard French (`fr-FR`) produces coherent text; regional variants like `fr-CD`
|
corpus: standard French (`fr-FR`) produces coherent text; regional variants like
|
||||||
and `fr-CI` are noticeably weaker; low-resource languages like Welsh, Māori, and
|
`fr-CD` and `fr-CI` are noticeably weaker; low-resource languages like Welsh,
|
||||||
Sicilian produce output that is syntactically plausible but often semantically
|
Māori, and Sicilian produce output that is syntactically plausible but often
|
||||||
broken.
|
semantically broken.
|
||||||
|
|
||||||
This is a property of the training distribution, not something that can be
|
This is a property of the training distribution, not something that can be
|
||||||
mitigated through prompt design. This is a well-documented characteristic of
|
mitigated through prompt design. This is a well-documented characteristic of
|
||||||
@@ -180,6 +180,13 @@ translated `description_local`. Output quality is reliable for high-resource
|
|||||||
languages such as French, though it may struggle with regional variants and
|
languages such as French, though it may struggle with regional variants and
|
||||||
idiomatic phrasing.
|
idiomatic phrasing.
|
||||||
|
|
||||||
|
For languages such as Welsh (Wales), Māori (Aotearoa/New Zealand), or Sicilian
|
||||||
|
(Sicily, Italy), the model can generate text that looks syntactically plausible
|
||||||
|
but is semantically incoherent. This comes from limited training-data coverage
|
||||||
|
rather than prompt engineering. The resource-tier gradient is illustrated by
|
||||||
|
this set of French-speaking cities, where `fr-FR` (Paris) produces coherent
|
||||||
|
output while `fr-CD` (Kinshasa) and `fr-CI` (Abidjan) do not:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -235,15 +242,6 @@ idiomatic phrasing.
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
This dataset, when fed into the pipeline will often times reason that a local
|
|
||||||
variant of French is needed, but will often times just default to a standardized
|
|
||||||
dialect of French, devoid of any cultural or linguistic nuance.
|
|
||||||
|
|
||||||
For languages such as Welsh (Wales), Māori (Aotearoa/New Zealand), or Sicilian
|
|
||||||
(Sicily, Italy), the model can generate text that looks syntactically plausible
|
|
||||||
but is semantically incoherent. This comes from limited training-data coverage
|
|
||||||
rather than prompt engineering.
|
|
||||||
|
|
||||||
Output sample:
|
Output sample:
|
||||||
[./out-sample/french-cities.example](out-sample/french-cities.example)
|
[./out-sample/french-cities.example](out-sample/french-cities.example)
|
||||||
|
|
||||||
@@ -263,12 +261,11 @@ Output sample:
|
|||||||
## Footnotes
|
## Footnotes
|
||||||
|
|
||||||
[^llm-choke]:
|
[^llm-choke]:
|
||||||
CHOKE (Certain Hallucinations Overriding Known Evidence) is a hallucination
|
See: "CHOKE: Certain Hallucinations Overriding Known Evidence," a term
|
||||||
failure mode defined by Simhi et al. (2025), in which a model that can
|
coined by AI researcher Gwern Branwen to describe a failure mode where a
|
||||||
consistently answer a question correctly produces a confident, wrong
|
model produces confident, coherent, and factually wrong output in response
|
||||||
response when the prompt is trivially perturbed. Source: Trust Me, I'm
|
to a specific-sounding prompt. Source:
|
||||||
Wrong: LLMs Hallucinate with Certainty Despite Knowing the Answer — Adi
|
[Trust Me, I'm Wrong: LLMs Hallucinate with Certainty Despite Knowing the Answer](https://arxiv.org/abs/2502.12964)
|
||||||
Simhi, Itay Itzhak, Fazl Barez, Gabriel Stanovsky, Yonatan Belinkov.
|
|
||||||
|
|
||||||
[^llm-bias]:
|
[^llm-bias]:
|
||||||
e.g., Blasi et al. (2022), "Systematic Inequalities in Language Technology
|
e.g., Blasi et al. (2022), "Systematic Inequalities in Language Technology
|
||||||
@@ -287,12 +284,9 @@ Output sample:
|
|||||||
[creativecommons.org/licenses/by-sa/4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en).
|
[creativecommons.org/licenses/by-sa/4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en).
|
||||||
|
|
||||||
[^diacetyl-source]:
|
[^diacetyl-source]:
|
||||||
White Labs confirms that diacetyl is a yeast-derived fermentation byproduct:
|
White Labs: "Diacetyl is a natural byproduct of fermentation… produced by
|
||||||
specifically, a compound produced during amino acid metabolism that leaks
|
brewer's yeast… generally considered undesirable at any perceived level."
|
||||||
out of the yeast cell and oxidises into its characteristic buttery
|
Source:
|
||||||
off-flavour. It is generally considered undesirable at any perceived level
|
|
||||||
in most styles, though low levels are tolerated in some English ales and
|
|
||||||
European lagers. Source:
|
|
||||||
[whitelabs.com — Compound Spotlight: Diacetyl](https://www.whitelabs.com/news-update-detail?id=54).
|
[whitelabs.com — Compound Spotlight: Diacetyl](https://www.whitelabs.com/news-update-detail?id=54).
|
||||||
|
|
||||||
[^diacetyl-rest]:
|
[^diacetyl-rest]:
|
||||||
@@ -6,7 +6,7 @@ descriptions via a local GGUF model or a deterministic mock.
|
|||||||
|
|
||||||
> **This pipeline produces AI-generated data.** It is not a source of truth for
|
> **This pipeline produces AI-generated data.** It is not a source of truth for
|
||||||
> brewing techniques, cultural representation, or local-language accuracy. See
|
> brewing techniques, cultural representation, or local-language accuracy. See
|
||||||
> [ETHICS-AND-KNOWN-ISSUES.md](./ETHICS-AND-KNOWN-ISSUES.md) for a full
|
> [ETHICS-AND-KNOWN-ISSUES.md](ETHICS-AND-KNOWN-ISSUES.md) for full
|
||||||
> documentation of limitations, hallucination patterns, and bias.
|
> documentation of limitations, hallucination patterns, and bias.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -18,7 +18,6 @@ descriptions via a local GGUF model or a deterministic mock.
|
|||||||
- [Build](#build)
|
- [Build](#build)
|
||||||
- [Model](#model)
|
- [Model](#model)
|
||||||
- [Run](#run)
|
- [Run](#run)
|
||||||
- [Docker / RunPod](#docker--runpod)
|
|
||||||
- [Architecture](#architecture)
|
- [Architecture](#architecture)
|
||||||
- [Pipeline Stages](#pipeline-stages)
|
- [Pipeline Stages](#pipeline-stages)
|
||||||
- [Key Components](#key-components)
|
- [Key Components](#key-components)
|
||||||
@@ -52,7 +51,7 @@ step.
|
|||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
Requirements: C++20 compiler, CMake 3.31+, OpenSSL, Boost (JSON and
|
Requirements: C++20 compiler, CMake 3.24+, libcurl, Boost (JSON and
|
||||||
ProgramOptions). SQLite is fetched from the upstream amalgamation, so no system
|
ProgramOptions). SQLite is fetched from the upstream amalgamation, so no system
|
||||||
SQLite package is required.
|
SQLite package is required.
|
||||||
|
|
||||||
@@ -61,16 +60,6 @@ cmake -S . -B build
|
|||||||
cmake --build build
|
cmake --build build
|
||||||
```
|
```
|
||||||
|
|
||||||
CMake automatically detects whether a compatible llama.cpp installation is
|
|
||||||
present on the system (`libllama`, `libggml`, `libggml-base`, and `llama.h`
|
|
||||||
visible on the default search paths). If found, it links against those
|
|
||||||
libraries and skips the FetchContent build. If not found, it fetches and builds
|
|
||||||
llama.cpp from source at tag `b9012`. No additional flags are required in
|
|
||||||
either case.
|
|
||||||
|
|
||||||
Metal is enabled automatically on Apple Silicon. CUDA or HIP/ROCm is detected
|
|
||||||
automatically on Linux when the relevant toolkit is present.
|
|
||||||
|
|
||||||
### Model
|
### Model
|
||||||
|
|
||||||
> Skip this step if you only need `--mocked`.
|
> Skip this step if you only need `--mocked`.
|
||||||
@@ -85,27 +74,20 @@ curl -L \
|
|||||||
### Run
|
### Run
|
||||||
|
|
||||||
Run from `build/` so the copied `locations.json` and `prompts/` are available.
|
Run from `build/` so the copied `locations.json` and `prompts/` are available.
|
||||||
Each run writes a fresh dated SQLite file such as
|
Each run also writes a fresh dated SQLite file such as
|
||||||
`biergarten_seed_2026-04-19T15-30-45.123456Z.sqlite` into the working directory.
|
`biergarten_seed_2026-04-19T15-30-45.123456Z.sqlite` into the working directory.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./biergarten-pipeline --mocked
|
./biergarten-pipeline --mocked
|
||||||
|
./biergarten-pipeline --model models/google_gemma-4-E4B-it-Q6_K.gguf --temperature 1.0 --top-p 0.95 --top-k 64 --n-ctx 8192 --seed -1
|
||||||
./biergarten-pipeline \
|
|
||||||
--model ../models/google_gemma-4-E4B-it-Q6_K.gguf \
|
|
||||||
--prompt-dir prompts \
|
|
||||||
--temperature 1.0 --top-p 0.95 --top-k 64 --n-ctx 8192 --seed -1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### CLI Flags
|
#### CLI Flags
|
||||||
|
|
||||||
| Flag | Purpose |
|
| Flag | Purpose |
|
||||||
| --------------- | ---------------------------------------------------------------------------------------------------- |
|
| --------------- | ------------------------------------------------------- |
|
||||||
| `--mocked` | Deterministic mock generator, no model required. |
|
| `--mocked` | Deterministic mock generator, no model required. |
|
||||||
| `--model, -m` | Path to a GGUF file. Required unless `--mocked` is set. |
|
| `--model, -m` | Path to a GGUF file. Required unless `--mocked` is set. |
|
||||||
| `--prompt-dir` | Directory containing prompt files (e.g. `BREWERY_GENERATION.md`). Required unless `--mocked` is set. |
|
|
||||||
| `--output, -o` | Directory for generated SQLite artifacts. Default: `output`. |
|
|
||||||
| `--log-path` | Path for application logs. Default: `pipeline.log`. |
|
|
||||||
| `--temperature` | Sampling temperature. Default: `1.0`. |
|
| `--temperature` | Sampling temperature. Default: `1.0`. |
|
||||||
| `--top-p` | Nucleus sampling. Default: `0.95`. |
|
| `--top-p` | Nucleus sampling. Default: `0.95`. |
|
||||||
| `--top-k` | Top-k sampling. Default: `64`. |
|
| `--top-k` | Top-k sampling. Default: `64`. |
|
||||||
@@ -118,91 +100,7 @@ error before the pipeline starts. Sampling flags are ignored when `--mocked` is
|
|||||||
set.
|
set.
|
||||||
|
|
||||||
The post-build step copies `prompts/` into `build/prompts/`. Rebuild after
|
The post-build step copies `prompts/` into `build/prompts/`. Rebuild after
|
||||||
editing any prompt file.
|
editing `prompts/system.md`.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Docker / RunPod
|
|
||||||
|
|
||||||
The `tooling/pipeline/runpod/` directory contains a GPU-ready container
|
|
||||||
configuration for running the pipeline on RunPod or any Docker host with an
|
|
||||||
NVIDIA GPU.
|
|
||||||
|
|
||||||
### How it works
|
|
||||||
|
|
||||||
The container uses a two-stage build. The first stage pulls prebuilt
|
|
||||||
`libllama`, `libggml`, and backend plugin libraries (including `libggml-cuda.so`
|
|
||||||
and the CPU variant plugins) from `ghcr.io/ggml-org/llama.cpp:full-cuda`. The
|
|
||||||
second stage copies those libraries into `/usr/local/lib` and runs `ldconfig` so
|
|
||||||
the dynamic linker and `dlopen` calls from `ggml_backend_load_all()` can resolve
|
|
||||||
the CUDA backend plugin at runtime. llama.cpp headers are cloned at the matching
|
|
||||||
tag and installed into `/usr/local/include`. CMake auto-detects both and skips
|
|
||||||
the FetchContent source build entirely, keeping image build times short.
|
|
||||||
|
|
||||||
`GGML_BACKEND_PATH` is set to `/usr/local/lib` so llama.cpp knows where to scan
|
|
||||||
for backend plugins.
|
|
||||||
|
|
||||||
### Build the image
|
|
||||||
|
|
||||||
Run from the `tooling/pipeline/` directory (the CMake project root), not from
|
|
||||||
inside `runpod/`, so the `COPY . .` step picks up the full project context.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t biergarten-pipeline:latest -f runpod/Dockerfile .
|
|
||||||
```
|
|
||||||
|
|
||||||
To monitor the full build output and confirm CMake selects the system llama.cpp:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build \
|
|
||||||
--progress=plain \
|
|
||||||
--no-cache \
|
|
||||||
-t biergarten-pipeline:latest \
|
|
||||||
-f runpod/Dockerfile \
|
|
||||||
. 2>&1 | tee build.log
|
|
||||||
```
|
|
||||||
|
|
||||||
Look for `[biergarten] Found system llama.cpp — skipping FetchContent` in the
|
|
||||||
output to confirm the fast path was taken.
|
|
||||||
|
|
||||||
### Run in mocked mode
|
|
||||||
|
|
||||||
No model or GPU required. Useful for validating the pipeline logic and SQLite
|
|
||||||
export path.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm \
|
|
||||||
-e BIERGARTEN_MODE=mocked \
|
|
||||||
-v "$PWD/output:/workspace/output" \
|
|
||||||
-v "$PWD/logs:/workspace/logs" \
|
|
||||||
biergarten-pipeline:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run in live mode
|
|
||||||
|
|
||||||
Mount your GGUF model before starting. The container validates the model path
|
|
||||||
before launching the binary.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm \
|
|
||||||
--runtime=nvidia \
|
|
||||||
-e BIERGARTEN_MODE=live \
|
|
||||||
-e GGML_BACKEND_PATH="/usr/local/lib/libggml-cuda.so" \
|
|
||||||
-v "$PWD/models:/workspace/models" \
|
|
||||||
-v "$PWD/output:/workspace/output" \
|
|
||||||
-v "$PWD/logs:/workspace/logs" \
|
|
||||||
biergarten-pipeline:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
The model must be present at `./models/google_gemma-4-E4B-it-Q6_K.gguf` on the
|
|
||||||
host. See [Model](#model) above for the download command.
|
|
||||||
|
|
||||||
### RunPod deployment
|
|
||||||
|
|
||||||
Use a GPU pod template. Mount persistent storage for `/workspace/models`,
|
|
||||||
`/workspace/output`, and `/workspace/logs`. Set `BIERGARTEN_MODE=live` in the
|
|
||||||
template environment. See `tooling/pipeline/runpod/pod-template.yaml` for a
|
|
||||||
starter template.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -299,18 +197,16 @@ code, latitude, and longitude for each entry.
|
|||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- C++20
|
- C++20
|
||||||
- CMake 3.31+
|
- CMake 3.24+
|
||||||
- Boost.JSON, Boost.ProgramOptions, Boost.DI
|
- Boost.JSON, Boost.ProgramOptions, Boost.DI
|
||||||
- spdlog
|
- spdlog
|
||||||
- cpp-httplib (with OpenSSL)
|
- libcurl
|
||||||
- SQLite amalgamation fetched and compiled via CMake FetchContent
|
- SQLite amalgamation fetched and compiled via CMake FetchContent
|
||||||
- llama.cpp (auto-detected from system install or fetched via FetchContent)
|
- llama.cpp
|
||||||
- Docker with NVIDIA CUDA 12.6 base image for GPU container builds
|
|
||||||
- RunPod for cloud GPU inference
|
|
||||||
|
|
||||||
The build fetches Boost.DI, spdlog, and SQLite via CMake. llama.cpp is fetched
|
The build fetches Boost.DI, spdlog, llama.cpp, and SQLite via CMake. Metal is
|
||||||
only when a system installation is not detected. Metal is enabled on Apple
|
enabled on Apple Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit
|
||||||
Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
is present.
|
||||||
|
|
||||||
> **Code Style:** Modern C++20 throughout — RAII for ownership,
|
> **Code Style:** Modern C++20 throughout — RAII for ownership,
|
||||||
> `std::unique_ptr` for injected dependencies, `std::optional` for parse
|
> `std::unique_ptr` for injected dependencies, `std::optional` for parse
|
||||||
@@ -322,7 +218,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
|||||||
|
|
||||||
## Tested Hardware
|
## Tested Hardware
|
||||||
|
|
||||||
### ARM macOS — M1 Pro
|
### ARM macOS - M1 Pro
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
| --------- | --------------------------------- |
|
| --------- | --------------------------------- |
|
||||||
@@ -333,7 +229,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
|||||||
| Model | Gemma 4 E4B |
|
| Model | Gemma 4 E4B |
|
||||||
| Inference | llama.cpp with Metal |
|
| Inference | llama.cpp with Metal |
|
||||||
|
|
||||||
### x86_64 Linux — NVIDIA RTX 2000
|
### x86_64 Linux - NVIDIA RTX 2000
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
| --------- | ------------------------------ |
|
| --------- | ------------------------------ |
|
||||||
@@ -344,15 +240,6 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
|||||||
| Model | Gemma 4 E4B |
|
| Model | Gemma 4 E4B |
|
||||||
| Inference | llama.cpp with CUDA 12.x |
|
| Inference | llama.cpp with CUDA 12.x |
|
||||||
|
|
||||||
### x86_64 Linux — Docker / RunPod (NVIDIA CUDA)
|
|
||||||
|
|
||||||
| | |
|
|
||||||
| --------- | ------------------------------------------- |
|
|
||||||
| Host | RunPod GPU pod |
|
|
||||||
| Base | nvidia/cuda:12.6.3-devel-ubuntu24.04 |
|
|
||||||
| Model | Gemma 4 E4B Q6_K |
|
|
||||||
| Inference | llama.cpp prebuilt CUDA backends via dlopen |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fixture Strategy
|
## Fixture Strategy
|
||||||
@@ -373,9 +260,8 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
|||||||
| `includes/` | Public headers and shared models. |
|
| `includes/` | Public headers and shared models. |
|
||||||
| `src/` | Implementation files. |
|
| `src/` | Implementation files. |
|
||||||
| `locations.json` | Curated city input copied into the build tree. |
|
| `locations.json` | Curated city input copied into the build tree. |
|
||||||
| `prompts/` | System prompts used by the model-backed path. |
|
| `prompts/` | System prompt used by the model-backed path. |
|
||||||
| `diagrams/` | Architecture and pipeline diagrams. |
|
| `diagrams/` | Architecture and pipeline diagrams. |
|
||||||
| `tooling/pipeline/runpod/` | Dockerfile, launcher, and RunPod pod template. |
|
|
||||||
| `ETHICS-AND-KNOWN-ISSUES.md` | Ethics, bias, hallucination analysis, mitigations. |
|
| `ETHICS-AND-KNOWN-ISSUES.md` | Ethics, bias, hallucination analysis, mitigations. |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -390,7 +276,6 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
|||||||
- `src/data_generation/llama/` — local inference, prompt loading, output
|
- `src/data_generation/llama/` — local inference, prompt loading, output
|
||||||
validation.
|
validation.
|
||||||
- `src/data_generation/mock/` — deterministic fallback.
|
- `src/data_generation/mock/` — deterministic fallback.
|
||||||
- `tooling/pipeline/runpod/` — container build and runtime launcher.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ if (Are arguments valid?) then (no)
|
|||||||
else (yes)
|
else (yes)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
:Init OpenSSL global state & LlamaBackendState;
|
:Init CurlGlobalState & LlamaBackendState;
|
||||||
:di::make_injector(...);
|
:di::make_injector(...);
|
||||||
:injector.create<std::unique_ptr<BiergartenDataGenerator>>();
|
:injector.create<std::unique_ptr<BiergartenDataGenerator>>();
|
||||||
:BiergartenDataGenerator::Run();
|
:BiergartenDataGenerator::Run();
|
||||||
@@ -26,7 +26,6 @@ skinparam note {
|
|||||||
title The Biergarten Data Pipeline - Class Diagram
|
title The Biergarten Data Pipeline - Class Diagram
|
||||||
|
|
||||||
class BiergartenDataGenerator {
|
class BiergartenDataGenerator {
|
||||||
- logger_ : std::shared_ptr<ILogger>
|
|
||||||
- context_service_ : std::unique_ptr<IEnrichmentService>
|
- context_service_ : std::unique_ptr<IEnrichmentService>
|
||||||
- generator_ : std::unique_ptr<DataGenerator>
|
- generator_ : std::unique_ptr<DataGenerator>
|
||||||
- exporter_ : std::unique_ptr<IExportService>
|
- exporter_ : std::unique_ptr<IExportService>
|
||||||
@@ -37,46 +36,6 @@ class BiergartenDataGenerator {
|
|||||||
- LogResults() : void
|
- LogResults() : void
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogLevel <<enumeration>> {
|
|
||||||
Debug
|
|
||||||
Info
|
|
||||||
Warn
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
|
|
||||||
class PipelinePhase <<enumeration>> {
|
|
||||||
Startup
|
|
||||||
UserGeneration
|
|
||||||
BreweryAndBeerGeneration
|
|
||||||
CheckinGeneration
|
|
||||||
RatingGeneration
|
|
||||||
FollowGeneration
|
|
||||||
Teardown
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LogEntry {
|
|
||||||
+ timestamp : std::chrono::system_clock::time_point
|
|
||||||
+ level : LogLevel
|
|
||||||
+ phase : PipelinePhase
|
|
||||||
+ message : std::string
|
|
||||||
+ worker : std::optional<std::string>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ILogger <<interface>> {
|
|
||||||
+ Log(entry : const LogEntry&) : void
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogProducer {
|
|
||||||
- channel_ : BoundedChannel<LogEntry>&
|
|
||||||
+ Log(entry : const LogEntry&) : void
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogDispatcher {
|
|
||||||
- channel_ : BoundedChannel<LogEntry>&
|
|
||||||
+ Run() : void
|
|
||||||
- ToSpdlogLevel(level) : spdlog::level::level_enum
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IEnrichmentService <<interface>> {
|
interface IEnrichmentService <<interface>> {
|
||||||
+ GetLocationContext(loc : const Location&) : std::string
|
+ GetLocationContext(loc : const Location&) : std::string
|
||||||
}
|
}
|
||||||
@@ -93,7 +52,7 @@ interface WebClient <<interface>> {
|
|||||||
+ UrlEncode(value : const std::string&) : std::string
|
+ UrlEncode(value : const std::string&) : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpWebClient {
|
class CURLWebClient {
|
||||||
+ Get(url : const std::string&) : std::string
|
+ Get(url : const std::string&) : std::string
|
||||||
+ UrlEncode(value : const std::string&) : std::string
|
+ UrlEncode(value : const std::string&) : std::string
|
||||||
}
|
}
|
||||||
@@ -164,21 +123,14 @@ class SystemDateTimeProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
' Structural Relationships / Dependency Injection
|
' Structural Relationships / Dependency Injection
|
||||||
BiergartenDataGenerator *-- ILogger : owns
|
|
||||||
BiergartenDataGenerator *-- IEnrichmentService : owns
|
BiergartenDataGenerator *-- IEnrichmentService : owns
|
||||||
BiergartenDataGenerator *-- DataGenerator : owns
|
BiergartenDataGenerator *-- DataGenerator : owns
|
||||||
BiergartenDataGenerator *-- IExportService : owns
|
BiergartenDataGenerator *-- IExportService : owns
|
||||||
|
|
||||||
LogEntry *-- LogLevel
|
|
||||||
LogEntry *-- PipelinePhase
|
|
||||||
ILogger <|.. LogProducer : implements
|
|
||||||
LogProducer ..> LogEntry : emits
|
|
||||||
LogDispatcher ..> LogEntry : consumes
|
|
||||||
|
|
||||||
IEnrichmentService <|.. WikipediaService : implements
|
IEnrichmentService <|.. WikipediaService : implements
|
||||||
WikipediaService *-- WebClient : owns
|
WikipediaService *-- WebClient : owns
|
||||||
|
|
||||||
WebClient <|.. HttpWebClient : implements
|
WebClient <|.. CURLWebClient : implements
|
||||||
|
|
||||||
DataGenerator <|.. MockGenerator : implements
|
DataGenerator <|.. MockGenerator : implements
|
||||||
DataGenerator <|.. LlamaGenerator : implements
|
DataGenerator <|.. LlamaGenerator : implements
|
||||||
262
pipeline/diagrams/planned/activity.puml
Normal file
262
pipeline/diagrams/planned/activity.puml
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
@startuml biergarten_activity
|
||||||
|
skinparam defaultFontName "DM Sans"
|
||||||
|
skinparam defaultFontSize 13
|
||||||
|
skinparam titleFontName "Volkhov"
|
||||||
|
skinparam titleFontSize 20
|
||||||
|
skinparam backgroundColor #FCFCF7
|
||||||
|
skinparam defaultFontColor #14180C
|
||||||
|
skinparam titleFontColor #14180C
|
||||||
|
skinparam ArrowColor #656F33
|
||||||
|
skinparam activityStartColor #EBECE3
|
||||||
|
skinparam activityEndColor #4A5837
|
||||||
|
skinparam activityStopColor #4A5837
|
||||||
|
skinparam ActivityBackgroundColor #EBECE3
|
||||||
|
skinparam ActivityBorderColor #4A5837
|
||||||
|
skinparam ActivityDiamondBackgroundColor #CBD2B5
|
||||||
|
skinparam ActivityDiamondBorderColor #4A5837
|
||||||
|
skinparam NoteBackgroundColor #DBEEDD
|
||||||
|
skinparam NoteFontColor #14180C
|
||||||
|
skinparam NoteBorderColor #4A5837
|
||||||
|
skinparam SwimlaneBorderColor #4A5837
|
||||||
|
skinparam SwimlaneBorderThickness 1
|
||||||
|
skinparam monochrome reverse
|
||||||
|
|
||||||
|
|
||||||
|
title The Biergarten Data Pipeline — Activity Diagram
|
||||||
|
|
||||||
|
|Main|
|
||||||
|
start
|
||||||
|
:ParseArguments(argc, argv);
|
||||||
|
if (Invalid args?) then (yes)
|
||||||
|
:spdlog::error;
|
||||||
|
stop
|
||||||
|
else (no)
|
||||||
|
endif
|
||||||
|
:Init CurlGlobalState & LlamaBackendState;
|
||||||
|
:Build DI injector;
|
||||||
|
|
||||||
|
|
||||||
|
:Initialize SqliteExportService;
|
||||||
|
note right
|
||||||
|
Opens SQLite connection.
|
||||||
|
Begins a single transaction
|
||||||
|
covering all five fixture types.
|
||||||
|
end note
|
||||||
|
|
||||||
|
:Create BoundedChannel<LogEntry> log_ch;
|
||||||
|
:Spawn Log Worker thread;
|
||||||
|
note right
|
||||||
|
Log worker drains log_ch for the
|
||||||
|
entire pipeline lifetime.
|
||||||
|
All workers emit LogEntry structs
|
||||||
|
via PipelineLogger — never spdlog directly.
|
||||||
|
end note
|
||||||
|
|
||||||
|
:BiergartenPipelineOrchestrator::Run();
|
||||||
|
|BiergartenPipelineOrchestrator::Run()|
|
||||||
|
:JsonLoader::LoadLocations("locations.json");
|
||||||
|
:JsonLoader::LoadBeerStyles("beer-styles.json");
|
||||||
|
:JsonLoader::LoadPersonas("personas.json");
|
||||||
|
:JsonLoader::LoadNamesByCountry("names-by-country.json");
|
||||||
|
|
||||||
|
:EnrichmentService::PreWarmBeerStyleCache(beer_styles);
|
||||||
|
note right
|
||||||
|
Beer styles do not need location context.
|
||||||
|
Wikipedia summaries for the entire palette are
|
||||||
|
fetched and cached globally at startup.
|
||||||
|
end note
|
||||||
|
|
||||||
|
:EnrichmentService::PreWarmPersonaCache(personas);
|
||||||
|
note right
|
||||||
|
Persona descriptions do not need location context.
|
||||||
|
All persona lookups are resolved and cached
|
||||||
|
globally at startup.
|
||||||
|
end note
|
||||||
|
|
||||||
|
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
' PHASE 0 — USER GENERATION
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
|Orchestrator|
|
||||||
|
:RunUserPhase(sampled_locations);
|
||||||
|
:Create BoundedChannels\n(loc_ch, llm_ch, exp_ch);
|
||||||
|
|
||||||
|
fork
|
||||||
|
|Orchestrator|
|
||||||
|
:Loop: Send Locations → loc_ch;
|
||||||
|
:Close loc_ch;
|
||||||
|
fork again
|
||||||
|
|LLM Worker|
|
||||||
|
while (loc_ch has items?) is (yes)
|
||||||
|
:Receive Location;
|
||||||
|
|
||||||
|
:IPersonaSelectionStrategy::SelectPersona(\n personas_palette_);
|
||||||
|
note right
|
||||||
|
Guaranteed cache hit from startup.
|
||||||
|
Returns a Persona struct carrying
|
||||||
|
style_affinities, abv_range,
|
||||||
|
ibu_preference, checkin_weight.
|
||||||
|
end note
|
||||||
|
|
||||||
|
:NamesByCountry::SampleName(\n location.iso3166_1);
|
||||||
|
note right
|
||||||
|
Deterministic lookup — no LLM involved.
|
||||||
|
Name selected from pre-keyed table
|
||||||
|
and passed into the generation prompt.
|
||||||
|
end note
|
||||||
|
|
||||||
|
:GenerateUser(location, persona, sampled_name)\nvia DataGenerator;
|
||||||
|
note right
|
||||||
|
LLM receives: Location fields + persona
|
||||||
|
description + sampled name. Generates
|
||||||
|
bio and preference signals grounded
|
||||||
|
in locale and persona.
|
||||||
|
end note
|
||||||
|
|
||||||
|
:PipelineLogger::Log(Info, UserGeneration,\n city, user_id, "llm");
|
||||||
|
:Send GeneratedUser → llm_ch;
|
||||||
|
endwhile (no)
|
||||||
|
:Close llm_ch;
|
||||||
|
fork again
|
||||||
|
|SQLite Worker|
|
||||||
|
while (llm_ch has items?) is (yes)
|
||||||
|
:Receive GeneratedUser;
|
||||||
|
:ProcessUser(user) → sqlite3_int64;
|
||||||
|
:PipelineLogger::Log(Info, UserGeneration,\n city, user_id, "sqlite");
|
||||||
|
:Append → user_pool_;
|
||||||
|
endwhile (no)
|
||||||
|
end fork
|
||||||
|
|
||||||
|
|Orchestrator|
|
||||||
|
:Join LLM Worker, SQLite Worker;
|
||||||
|
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
' PHASE 1 — BREWERY & BEER GENERATION
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
:RunBreweryAndBeerPhase(sampled_locations);
|
||||||
|
:Create BoundedChannels\n(loc_ch, llm_ch, exp_ch);
|
||||||
|
|
||||||
|
fork
|
||||||
|
|Orchestrator|
|
||||||
|
:Loop: Send Locations → loc_ch;
|
||||||
|
:Close loc_ch;
|
||||||
|
fork again
|
||||||
|
|Enrichment Workers (xN)|
|
||||||
|
while (loc_ch has items?) is (yes)
|
||||||
|
:Receive Location;
|
||||||
|
:GetLocationContext(location,\nBreweryContextStrategy);
|
||||||
|
:PipelineLogger::Log(Info,\n BreweryAndBeerGeneration,\n city, nullopt, "enrichment");
|
||||||
|
:Send EnrichedCity → llm_ch;
|
||||||
|
endwhile (no)
|
||||||
|
|Orchestrator|
|
||||||
|
:Join Enrichment Workers;
|
||||||
|
:Close llm_ch;
|
||||||
|
fork again
|
||||||
|
|LLM Worker|
|
||||||
|
while (llm_ch has items?) is (yes)
|
||||||
|
:Receive EnrichedCity;
|
||||||
|
|
||||||
|
:GenerateBrewery(location, context)\nvia DataGenerator;
|
||||||
|
|
||||||
|
:IBeerSelectionStrategy::SelectStyles(\n brewery, beer_style_palette_);
|
||||||
|
|
||||||
|
while (For each selected BeerStyle?) is (remaining)
|
||||||
|
:GetStyleContextFromCache(style);
|
||||||
|
note right
|
||||||
|
Guaranteed cache hit from startup.
|
||||||
|
end note
|
||||||
|
:GenerateBeer(brewery, style_context)\nvia DataGenerator;
|
||||||
|
:Attach GeneratedBeer to Brewery bundle;
|
||||||
|
endwhile (done)
|
||||||
|
|
||||||
|
:PipelineLogger::Log(Info,\n BreweryAndBeerGeneration,\n city, brewery_id, "llm");
|
||||||
|
:Send BreweryWithBeers Bundle → exp_ch;
|
||||||
|
endwhile (no)
|
||||||
|
:Close exp_ch;
|
||||||
|
fork again
|
||||||
|
|SQLite Worker|
|
||||||
|
while (exp_ch has items?) is (yes)
|
||||||
|
:Receive BreweryWithBeers Bundle;
|
||||||
|
:ProcessBrewery(brewery) → brewery_id;
|
||||||
|
:Append → brewery_pool_;
|
||||||
|
|
||||||
|
while (For each beer in bundle?) is (remaining)
|
||||||
|
:Set beer.brewery_id = brewery_id;
|
||||||
|
:ProcessBeer(beer) → sqlite3_int64;
|
||||||
|
:Append → beer_pool_;
|
||||||
|
endwhile (done)
|
||||||
|
|
||||||
|
:PipelineLogger::Log(Info,\n BreweryAndBeerGeneration,\n city, brewery_id, "sqlite");
|
||||||
|
endwhile (no)
|
||||||
|
end fork
|
||||||
|
|
||||||
|
|Orchestrator|
|
||||||
|
:Join LLM Worker, SQLite Worker;
|
||||||
|
note right
|
||||||
|
Both brewery_pool_ and beer_pool_
|
||||||
|
are now completely populated.
|
||||||
|
end note
|
||||||
|
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
' PHASE 2 — CHECKIN GENERATION
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
:RunCheckinPhase();
|
||||||
|
:ICheckinDistributionStrategy::\nAssignActivityWeights(user_pool_);
|
||||||
|
note right
|
||||||
|
Weights seeded from each user's
|
||||||
|
persona.checkin_weight. J-curve profile
|
||||||
|
emerges from persona distribution.
|
||||||
|
end note
|
||||||
|
|
||||||
|
while (For each GeneratedUser in user_pool_?) is (remaining)
|
||||||
|
:CheckinsForUser(user, brewery_pool_.size());
|
||||||
|
while (For each checkin index?) is (remaining)
|
||||||
|
:TimestampFor(user, index);
|
||||||
|
:Select brewery from brewery_pool_;
|
||||||
|
:GenerateCheckin(user, brewery, timestamp)\nvia DataGenerator;
|
||||||
|
:ProcessCheckin(checkin) → sqlite3_int64;
|
||||||
|
:PipelineLogger::Log(Info, CheckinGeneration,\n nullopt, checkin_id, "sqlite");
|
||||||
|
:Append → checkin_pool_;
|
||||||
|
endwhile (done)
|
||||||
|
endwhile (done)
|
||||||
|
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
' PHASE 3 — RATING GENERATION
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
:RunRatingPhase();
|
||||||
|
note right
|
||||||
|
Beer selection biased by
|
||||||
|
user.persona.style_affinities and abv_range.
|
||||||
|
Rating skew modulated per persona.
|
||||||
|
end note
|
||||||
|
|
||||||
|
while (For each GeneratedCheckin in checkin_pool_?) is (remaining)
|
||||||
|
:Match brewery_id → select beer from beer_pool_\n(same brewery_id, biased by persona affinities);
|
||||||
|
if (Beer exists for brewery?) then (yes)
|
||||||
|
:GenerateRating(user, beer, checkin_id)\nvia DataGenerator;
|
||||||
|
:ProcessRating(rating);
|
||||||
|
:PipelineLogger::Log(Info, RatingGeneration,\n nullopt, rating_id, "sqlite");
|
||||||
|
else (no)
|
||||||
|
:PipelineLogger::Log(Warn, RatingGeneration,\n nullopt, brewery_id, "sqlite");
|
||||||
|
:Skip — brewery has no beers;
|
||||||
|
endif
|
||||||
|
endwhile (done)
|
||||||
|
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
' TEARDOWN
|
||||||
|
' ═══════════════════════════════════════════
|
||||||
|
|Main|
|
||||||
|
:Finalize SqliteExportService;
|
||||||
|
note right
|
||||||
|
COMMIT covers all five fixture types.
|
||||||
|
end note
|
||||||
|
:Close log_ch;
|
||||||
|
:Join Log Worker;
|
||||||
|
note right
|
||||||
|
Drain guarantees no LogEntry is
|
||||||
|
dropped at shutdown.
|
||||||
|
end note
|
||||||
|
:spdlog::info "Pipeline complete in X ms";
|
||||||
|
stop
|
||||||
|
|
||||||
|
@enduml
|
||||||
@@ -1,16 +1,51 @@
|
|||||||
@startuml class_diagram
|
@startuml future_possible_architecture
|
||||||
|
|
||||||
' ==========================================
|
' ==========================================
|
||||||
' CONFIGURATION & STYLING
|
' CONFIGURATION & STYLING
|
||||||
' ==========================================
|
' ==========================================
|
||||||
!include ../biergarten-weizen-theme.puml
|
left to right direction
|
||||||
skinparam classAttributeFontSize 9
|
skinparam linetype ortho
|
||||||
skinparam defaultFontSize 25
|
|
||||||
skinparam titleFontSize 30
|
|
||||||
|
|
||||||
title Biergarten Data Pipeline — Class Diagram
|
' --- Typography ---
|
||||||
|
skinparam defaultFontName "DM Sans"
|
||||||
|
skinparam defaultFontSize 14
|
||||||
|
skinparam titleFontName "Volkhov"
|
||||||
|
skinparam titleFontSize 20
|
||||||
|
|
||||||
package "Domain: Models" {
|
' --- Global Colors ---
|
||||||
|
skinparam backgroundColor #FCFCF7
|
||||||
|
skinparam defaultFontColor #14180C
|
||||||
|
skinparam titleFontColor #14180C
|
||||||
|
skinparam ArrowColor #656F33
|
||||||
|
|
||||||
|
skinparam class {
|
||||||
|
BackgroundColor #EBECE3
|
||||||
|
HeaderBackgroundColor #CBD2B5
|
||||||
|
BorderColor #4A5837
|
||||||
|
ArrowColor #656F33
|
||||||
|
FontColor #14180C
|
||||||
|
}
|
||||||
|
|
||||||
|
skinparam package {
|
||||||
|
BackgroundColor #DBEEDD
|
||||||
|
BorderColor #4A5837
|
||||||
|
FontColor #14180C
|
||||||
|
}
|
||||||
|
|
||||||
|
skinparam note {
|
||||||
|
BackgroundColor #DBEEDD
|
||||||
|
BorderColor #4A5837
|
||||||
|
FontColor #14180C
|
||||||
|
}
|
||||||
|
|
||||||
|
skinparam monochrome reverse
|
||||||
|
|
||||||
|
title The Biergarten Data Pipeline — Planned Architecture
|
||||||
|
|
||||||
|
' ==========================================
|
||||||
|
' DOMAIN MODELS
|
||||||
|
' ==========================================
|
||||||
|
package "Domain Models" {
|
||||||
|
|
||||||
class Location {
|
class Location {
|
||||||
+ city : std::string
|
+ city : std::string
|
||||||
@@ -27,9 +62,8 @@ package "Domain: Models" {
|
|||||||
+ text : std::string
|
+ text : std::string
|
||||||
+ completeness : Completeness
|
+ completeness : Completeness
|
||||||
+ char_count : size_t
|
+ char_count : size_t
|
||||||
}
|
--
|
||||||
|
<<enum>> Completeness
|
||||||
enum Completeness {
|
|
||||||
Full
|
Full
|
||||||
Partial
|
Partial
|
||||||
Absent
|
Absent
|
||||||
@@ -82,69 +116,46 @@ package "Domain: Models" {
|
|||||||
+ note : std::string
|
+ note : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenerationMetadata {
|
|
||||||
+ generation_id : uint64_t
|
|
||||||
+ generated_time : std::string
|
|
||||||
+ context_provided : bool
|
|
||||||
+ generated_with : std::string
|
|
||||||
}
|
|
||||||
|
|
||||||
class GeneratedBrewery {
|
class GeneratedBrewery {
|
||||||
+ brewery_id : uint64_t
|
+ brewery_id : sqlite3_int64
|
||||||
+ location : Location
|
+ location : Location
|
||||||
+ brewery : BreweryResult
|
+ brewery : BreweryResult
|
||||||
+ context_completeness : LocationContext::Completeness
|
+ context_completeness : LocationContext::Completeness
|
||||||
+ metadata : GenerationMetadata
|
+ generated_at : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedBeer {
|
class GeneratedBeer {
|
||||||
+ beer_id : uint64_t
|
+ beer_id : sqlite3_int64
|
||||||
+ brewery_id : uint64_t
|
+ brewery_id : sqlite3_int64
|
||||||
+ location : Location
|
+ location : Location
|
||||||
+ style : BeerStyle
|
+ style : BeerStyle
|
||||||
+ beer : BeerResult
|
+ beer : BeerResult
|
||||||
+ metadata : GenerationMetadata
|
+ generated_at : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedUser {
|
class GeneratedUser {
|
||||||
+ user_id : uint64_t
|
+ user_id : sqlite3_int64
|
||||||
+ location : Location
|
+ location : Location
|
||||||
+ user : UserResult
|
+ user : UserResult
|
||||||
+ metadata : GenerationMetadata
|
+ generated_at : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedCheckin {
|
class GeneratedCheckin {
|
||||||
+ checkin_id : uint64_t
|
+ checkin_id : sqlite3_int64
|
||||||
+ user_id : uint64_t
|
+ user_id : sqlite3_int64
|
||||||
+ brewery_id : uint64_t
|
+ brewery_id : sqlite3_int64
|
||||||
+ checkin : CheckinResult
|
+ checkin : CheckinResult
|
||||||
+ metadata : GenerationMetadata
|
+ generated_at : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedRating {
|
class GeneratedRating {
|
||||||
+ user_id : uint64_t
|
+ user_id : sqlite3_int64
|
||||||
+ beer_id : uint64_t
|
+ beer_id : sqlite3_int64
|
||||||
+ checkin_id : uint64_t
|
+ checkin_id : sqlite3_int64
|
||||||
+ rating : RatingResult
|
+ rating : RatingResult
|
||||||
+ metadata : GenerationMetadata
|
+ generated_at : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedFollow {
|
|
||||||
+ follower_id : uint64_t
|
|
||||||
+ followed_id : uint64_t
|
|
||||||
+ metadata : GenerationMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserPersona {
|
|
||||||
+ name: std::string
|
|
||||||
+ description: std::string
|
|
||||||
+ style_affinities: std::vector<std::string>
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationContext *-- Completeness
|
|
||||||
}
|
|
||||||
|
|
||||||
package "Domain: Application Configuration" {
|
|
||||||
class SamplingOptions {
|
class SamplingOptions {
|
||||||
+ temperature : float = 1.0F
|
+ temperature : float = 1.0F
|
||||||
+ top_p : float = 0.95F
|
+ top_p : float = 0.95F
|
||||||
@@ -156,7 +167,7 @@ package "Domain: Application Configuration" {
|
|||||||
class GeneratorOptions {
|
class GeneratorOptions {
|
||||||
+ model_path : std::filesystem::path
|
+ model_path : std::filesystem::path
|
||||||
+ use_mocked : bool = false
|
+ use_mocked : bool = false
|
||||||
+ sampling: std::optional<SamplingOptions>
|
+ sampling : SamplingOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
class PipelineOptions {
|
class PipelineOptions {
|
||||||
@@ -169,12 +180,74 @@ package "Domain: Application Configuration" {
|
|||||||
+ pipeline : PipelineOptions
|
+ pipeline : PipelineOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
' --- Domain Model Relationships ---
|
||||||
ApplicationOptions *-- GeneratorOptions
|
ApplicationOptions *-- GeneratorOptions
|
||||||
ApplicationOptions *-- PipelineOptions
|
ApplicationOptions *-- PipelineOptions
|
||||||
GeneratorOptions o-- SamplingOptions
|
GeneratorOptions *-- SamplingOptions
|
||||||
|
LocationContext *-- Completeness
|
||||||
}
|
}
|
||||||
|
|
||||||
package "Domain: Policy" {
|
|
||||||
|
' ==========================================
|
||||||
|
' LOGGING
|
||||||
|
' ==========================================
|
||||||
|
package "Logging" {
|
||||||
|
|
||||||
|
enum LogLevel {
|
||||||
|
Debug
|
||||||
|
Info
|
||||||
|
Warn
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PipelinePhase {
|
||||||
|
Startup
|
||||||
|
UserGeneration
|
||||||
|
BreweryAndBeerGeneration
|
||||||
|
CheckinGeneration
|
||||||
|
RatingGeneration
|
||||||
|
Teardown
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogEntry {
|
||||||
|
+ timestamp : std::chrono::system_clock::time_point
|
||||||
|
+ level : LogLevel
|
||||||
|
+ phase : PipelinePhase
|
||||||
|
+ message : std::string
|
||||||
|
+ city : std::optional<std::string>
|
||||||
|
+ entity_id : std::optional<std::string>
|
||||||
|
+ worker : std::optional<std::string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Logger <<interface>> {
|
||||||
|
+ Log(level, phase, message,\n city, entity_id, worker) : void
|
||||||
|
}
|
||||||
|
|
||||||
|
class PipelineLogger {
|
||||||
|
- log_ch_ : BoundedChannel<LogEntry>&
|
||||||
|
+ Log(level, phase, message,\n city, entity_id, worker) : void
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogWorker {
|
||||||
|
- log_ch_ : BoundedChannel<LogEntry>&
|
||||||
|
+ Run() : void
|
||||||
|
- FormatTimestamp(tp) : std::string
|
||||||
|
- ToSpdlogLevel(level) : spdlog::level::level_enum
|
||||||
|
- ToString(phase) : std::string
|
||||||
|
}
|
||||||
|
|
||||||
|
' --- Logging Relationships ---
|
||||||
|
LogEntry *-- LogLevel
|
||||||
|
LogEntry *-- PipelinePhase
|
||||||
|
PipelineLogger ..> LogEntry : emits
|
||||||
|
LogWorker ..> LogEntry : consumes
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
' ==========================================
|
||||||
|
' DOMAIN POLICY
|
||||||
|
' ==========================================
|
||||||
|
package "Domain Policy" {
|
||||||
|
|
||||||
interface ContextStrategy <<interface>> {
|
interface ContextStrategy <<interface>> {
|
||||||
+ QueriesFor(loc : const Location&) : std::vector<std::string>
|
+ QueriesFor(loc : const Location&) : std::vector<std::string>
|
||||||
@@ -224,99 +297,13 @@ package "Domain: Policy" {
|
|||||||
+ TimestampFor(user : const GeneratedUser&,\n index : size_t) : std::string
|
+ TimestampFor(user : const GeneratedUser&,\n index : size_t) : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class RandomCheckinStrategy {
|
|
||||||
- rng_ : std::mt19937
|
|
||||||
- min_checkins_ : size_t
|
|
||||||
- max_checkins_ : size_t
|
|
||||||
+ AssignActivityWeights(users : std::vector<GeneratedUser>&) : void
|
|
||||||
+ CheckinsForUser(user : const GeneratedUser&,\n brewery_count : size_t) : size_t
|
|
||||||
+ TimestampFor(user : const GeneratedUser&,\n index : size_t) : std::string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FollowGenerationStrategy <<interface>> {
|
|
||||||
+ GenerateFollows(users : const std::vector<GeneratedUser>&) : std::vector<GeneratedFollow>
|
|
||||||
}
|
|
||||||
|
|
||||||
class RandomFollowStrategy {
|
' ==========================================
|
||||||
- rng_ : std::mt19937
|
' ORCHESTRATION
|
||||||
- min_follows_ : size_t
|
' ==========================================
|
||||||
- max_follows_ : size_t
|
package "Orchestration" {
|
||||||
+ GenerateFollows(users : const std::vector<GeneratedUser>&) : std::vector<GeneratedFollow>
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActivityWeightedFollowStrategy {
|
|
||||||
- rng_ : std::mt19937
|
|
||||||
- min_follows_ : size_t
|
|
||||||
- max_follows_ : size_t
|
|
||||||
+ GenerateFollows(users : const std::vector<GeneratedUser>&) : std::vector<GeneratedFollow>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
package "Infrastructure: Logging" {
|
|
||||||
enum LogLevel {
|
|
||||||
Debug
|
|
||||||
Info
|
|
||||||
Warn
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PipelinePhase {
|
|
||||||
Startup
|
|
||||||
UserGeneration
|
|
||||||
BreweryAndBeerGeneration
|
|
||||||
CheckinGeneration
|
|
||||||
RatingGeneration
|
|
||||||
FollowGeneration
|
|
||||||
Teardown
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogEntry {
|
|
||||||
+ timestamp : std::chrono::system_clock::time_point
|
|
||||||
+ level : LogLevel
|
|
||||||
+ phase : PipelinePhase
|
|
||||||
+ message : std::string
|
|
||||||
+ worker : std::optional<std::string>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ILogger <<interface>> {
|
|
||||||
+ Log(entry : const LogEntry&) : void
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogProducer {
|
|
||||||
- channel_ : BoundedChannel<LogEntry>&
|
|
||||||
+ Log(entry : const LogEntry&) : void
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogDispatcher {
|
|
||||||
- channel_ : BoundedChannel<LogEntry>&
|
|
||||||
+ Run() : void
|
|
||||||
- ToSpdlogLevel(level) : spdlog::level::level_enum
|
|
||||||
}
|
|
||||||
|
|
||||||
LogEntry *-- LogLevel
|
|
||||||
LogEntry *-- PipelinePhase
|
|
||||||
ILogger <|.. LogProducer
|
|
||||||
LogProducer ..> LogEntry : emits
|
|
||||||
LogDispatcher ..> LogEntry : consumes
|
|
||||||
}
|
|
||||||
|
|
||||||
package "Infrastructure: Pipeline Channel" {
|
|
||||||
|
|
||||||
class "BoundedChannel<T>" as BoundedChannel {
|
|
||||||
- queue_ : std::queue<T>
|
|
||||||
- mutex_ : std::mutex
|
|
||||||
- not_full_ : std::condition_variable
|
|
||||||
- not_empty_ : std::condition_variable
|
|
||||||
- capacity_ : size_t
|
|
||||||
- closed_ : bool
|
|
||||||
+ Send(item : T) : void
|
|
||||||
+ Receive() : std::optional<T>
|
|
||||||
+ Close() : void
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
package "Infrastructure: Data Preloading" {
|
|
||||||
|
|
||||||
interface DataPreloader <<interface>> {
|
interface DataPreloader <<interface>> {
|
||||||
+ LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location>
|
+ LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location>
|
||||||
@@ -325,6 +312,38 @@ package "Infrastructure: Data Preloading" {
|
|||||||
+ LoadNamesByCountry(filepath : const std::filesystem::path&) : NamesByCountry
|
+ LoadNamesByCountry(filepath : const std::filesystem::path&) : NamesByCountry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BiergartenPipelineOrchestrator {
|
||||||
|
- preloader_ : std::unique_ptr<DataPreloader>
|
||||||
|
- enrichment_service_ : std::unique_ptr<EnrichmentService>
|
||||||
|
- generator_ : std::unique_ptr<DataGenerator>
|
||||||
|
- logger_ : std::unique_ptr<Logger>
|
||||||
|
- exporter_ : std::unique_ptr<ExportService>
|
||||||
|
- brewery_context_strategy_ : std::unique_ptr<ContextStrategy>
|
||||||
|
- sampling_strategy_ : std::unique_ptr<SamplingStrategy>
|
||||||
|
- beer_selection_strategy_ : std::unique_ptr<BeerSelectionStrategy>
|
||||||
|
- checkin_strategy_ : std::unique_ptr<CheckinDistributionStrategy>
|
||||||
|
- beer_style_palette_ : std::vector<BeerStyle>
|
||||||
|
- options_ : ApplicationOptions
|
||||||
|
--
|
||||||
|
- user_pool_ : std::vector<GeneratedUser>
|
||||||
|
- brewery_pool_ : std::vector<GeneratedBrewery>
|
||||||
|
- beer_pool_ : std::vector<GeneratedBeer>
|
||||||
|
- checkin_pool_ : std::vector<GeneratedCheckin>
|
||||||
|
--
|
||||||
|
+ Run() : bool
|
||||||
|
- RunUserPhase(locations : const std::vector<Location>&) : void
|
||||||
|
- RunBreweryAndBeerPhase(locations : const std::vector<Location>&) : void
|
||||||
|
- RunCheckinPhase() : void
|
||||||
|
- RunRatingPhase() : void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
' ==========================================
|
||||||
|
' INFRASTRUCTURE: PRELOADING
|
||||||
|
' ==========================================
|
||||||
|
package "Infrastructure: Preloading" {
|
||||||
|
|
||||||
class JsonLoader {
|
class JsonLoader {
|
||||||
+ LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location>
|
+ LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location>
|
||||||
+ LoadBeerStyles(filepath : const std::filesystem::path&) : std::vector<BeerStyle>
|
+ LoadBeerStyles(filepath : const std::filesystem::path&) : std::vector<BeerStyle>
|
||||||
@@ -334,6 +353,10 @@ package "Infrastructure: Data Preloading" {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
' ==========================================
|
||||||
|
' INFRASTRUCTURE: ENRICHMENT
|
||||||
|
' ==========================================
|
||||||
package "Infrastructure: Enrichment" {
|
package "Infrastructure: Enrichment" {
|
||||||
|
|
||||||
interface EnrichmentService <<interface>> {
|
interface EnrichmentService <<interface>> {
|
||||||
@@ -352,37 +375,25 @@ package "Infrastructure: Enrichment" {
|
|||||||
+ UrlEncode(value : const std::string&) : std::string
|
+ UrlEncode(value : const std::string&) : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpWebClient {
|
class CURLWebClient {
|
||||||
+ Get(url : const std::string&) : std::string
|
+ Get(url : const std::string&) : std::string
|
||||||
+ UrlEncode(value : const std::string&) : std::string
|
+ UrlEncode(value : const std::string&) : std::string
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
package "Infrastructure: Prompting" {
|
|
||||||
|
|
||||||
interface IPromptDirectory <<interface>> {
|
' ==========================================
|
||||||
+ Load(key : std::string_view) : std::string
|
' INFRASTRUCTURE: GENERATION
|
||||||
}
|
' ==========================================
|
||||||
|
package "Infrastructure: Generation" {
|
||||||
class PromptDirectory {
|
|
||||||
- prompt_dir_ : std::filesystem::path
|
|
||||||
- cache_ : std::unordered_map<std::string, std::string>
|
|
||||||
+ PromptDirectory(prompt_dir : const std::filesystem::path&)
|
|
||||||
+ Load(key : std::string_view) : std::string
|
|
||||||
}
|
|
||||||
|
|
||||||
IPromptDirectory <|.. PromptDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
package "Infrastructure: Data Generation" {
|
|
||||||
|
|
||||||
interface DataGenerator <<interface>> {
|
interface DataGenerator <<interface>> {
|
||||||
+ GenerateBrewery(location : const Location&,\n context : const LocationContext&) : BreweryResult
|
+ GenerateBrewery(location : const Location&,\n context : const LocationContext&) : BreweryResult
|
||||||
+ GenerateBeer(brewery_id : uint64_t,\n location : const Location&,\n context : const LocationContext&,\n style : const BeerStyle&) : BeerResult
|
+ GenerateBeer(brewery_id : sqlite3_int64,\n location : const Location&,\n context : const LocationContext&,\n style : const BeerStyle&) : BeerResult
|
||||||
+ GenerateUser(location : const Location&) : UserResult
|
+ GenerateUser(location : const Location&) : UserResult
|
||||||
+ GenerateCheckin(user : const GeneratedUser&,\n brewery : const GeneratedBrewery&,\n timestamp : const std::string&) : CheckinResult
|
+ GenerateCheckin(user : const GeneratedUser&,\n brewery : const GeneratedBrewery&,\n timestamp : const std::string&) : CheckinResult
|
||||||
+ GenerateRating(user : const GeneratedUser&,\n beer : const GeneratedBeer&,\n checkin_id : uint64_t) : RatingResult
|
+ GenerateRating(user : const GeneratedUser&,\n beer : const GeneratedBeer&,\n checkin_id : sqlite3_int64) : RatingResult
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockGenerator {
|
class MockGenerator {
|
||||||
@@ -398,7 +409,6 @@ package "Infrastructure: Data Generation" {
|
|||||||
- model_ : ModelHandle
|
- model_ : ModelHandle
|
||||||
- context_ : ContextHandle
|
- context_ : ContextHandle
|
||||||
- prompt_formatter_ : std::unique_ptr<PromptFormatter>
|
- prompt_formatter_ : std::unique_ptr<PromptFormatter>
|
||||||
- prompt_directory_ : std::unique_ptr<IPromptDirectory>
|
|
||||||
- rng_ : std::mt19937
|
- rng_ : std::mt19937
|
||||||
+ GenerateBrewery(...) : BreweryResult
|
+ GenerateBrewery(...) : BreweryResult
|
||||||
+ GenerateBeer(...) : BeerResult
|
+ GenerateBeer(...) : BeerResult
|
||||||
@@ -422,16 +432,39 @@ package "Infrastructure: Data Generation" {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
package "Infrastructure: Data Export" {
|
|
||||||
|
' ==========================================
|
||||||
|
' INFRASTRUCTURE: PIPELINE CHANNEL
|
||||||
|
' ==========================================
|
||||||
|
package "Infrastructure: Pipeline Channel" {
|
||||||
|
|
||||||
|
class "BoundedChannel<T>" as BoundedChannel {
|
||||||
|
- queue_ : std::queue<T>
|
||||||
|
- mutex_ : std::mutex
|
||||||
|
- not_full_ : std::condition_variable
|
||||||
|
- not_empty_ : std::condition_variable
|
||||||
|
- capacity_ : size_t
|
||||||
|
- closed_ : bool
|
||||||
|
+ Send(item : T) : void
|
||||||
|
+ Receive() : std::optional<T>
|
||||||
|
+ Close() : void
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
' ==========================================
|
||||||
|
' INFRASTRUCTURE: EXPORT
|
||||||
|
' ==========================================
|
||||||
|
package "Infrastructure: Export" {
|
||||||
|
|
||||||
interface ExportService <<interface>> {
|
interface ExportService <<interface>> {
|
||||||
+ Initialize() : void
|
+ Initialize() : void
|
||||||
+ ProcessBrewery(brewery : const GeneratedBrewery&) : uint64_t
|
+ ProcessBrewery(brewery : const GeneratedBrewery&) : sqlite3_int64
|
||||||
+ ProcessBeer(beer : const GeneratedBeer&) : uint64_t
|
+ ProcessBeer(beer : const GeneratedBeer&) : sqlite3_int64
|
||||||
+ ProcessUser(user : const GeneratedUser&) : uint64_t
|
+ ProcessUser(user : const GeneratedUser&) : sqlite3_int64
|
||||||
+ ProcessCheckin(checkin : const GeneratedCheckin&) : uint64_t
|
+ ProcessCheckin(checkin : const GeneratedCheckin&) : sqlite3_int64
|
||||||
+ ProcessRating(rating : const GeneratedRating&) : void
|
+ ProcessRating(rating : const GeneratedRating&) : void
|
||||||
+ ProcessFollow(follow : const GeneratedFollow&) : void
|
|
||||||
+ Finalize() : void
|
+ Finalize() : void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,17 +477,15 @@ package "Infrastructure: Data Export" {
|
|||||||
- insert_user_stmt_ : SqliteStatementHandle
|
- insert_user_stmt_ : SqliteStatementHandle
|
||||||
- insert_checkin_stmt_ : SqliteStatementHandle
|
- insert_checkin_stmt_ : SqliteStatementHandle
|
||||||
- insert_rating_stmt_ : SqliteStatementHandle
|
- insert_rating_stmt_ : SqliteStatementHandle
|
||||||
- insert_follow_stmt_ : SqliteStatementHandle
|
|
||||||
- transaction_open_ : bool
|
- transaction_open_ : bool
|
||||||
- location_cache_ : std::unordered_map<std::string, uint64_t>
|
- location_cache_ : std::unordered_map<std::string, sqlite3_int64>
|
||||||
- brewery_cache_ : std::unordered_map<std::string, uint64_t>
|
- brewery_cache_ : std::unordered_map<std::string, sqlite3_int64>
|
||||||
+ Initialize() : void
|
+ Initialize() : void
|
||||||
+ ProcessRecord(brewery : const GeneratedBrewery&) : uint64_t
|
+ ProcessBrewery(brewery : const GeneratedBrewery&) : sqlite3_int64
|
||||||
+ ProcessRecord(beer : const GeneratedBeer&) : uint64_t
|
+ ProcessBeer(beer : const GeneratedBeer&) : sqlite3_int64
|
||||||
+ ProcessRecord(user : const GeneratedUser&) : uint64_t
|
+ ProcessUser(user : const GeneratedUser&) : sqlite3_int64
|
||||||
+ ProcessRecord(checkin : const GeneratedCheckin&) : uint64_t
|
+ ProcessCheckin(checkin : const GeneratedCheckin&) : sqlite3_int64
|
||||||
+ ProcessRecord(rating : const GeneratedRating&) : void
|
+ ProcessRating(rating : const GeneratedRating&) : void
|
||||||
+ ProcessRecord(follow : const GeneratedFollow&) : void
|
|
||||||
+ Finalize() : void
|
+ Finalize() : void
|
||||||
- InitializeSchema() : void
|
- InitializeSchema() : void
|
||||||
- PrepareStatements() : void
|
- PrepareStatements() : void
|
||||||
@@ -472,33 +503,10 @@ package "Infrastructure: Data Export" {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class BiergartenPipelineOrchestrator {
|
|
||||||
- preloader_ : std::unique_ptr<DataPreloader>
|
' ==========================================
|
||||||
- enrichment_service_ : std::unique_ptr<EnrichmentService>
|
' GLOBAL RELATIONSHIPS
|
||||||
- generator_ : std::unique_ptr<DataGenerator>
|
' ==========================================
|
||||||
- logger_ : std::unique_ptr<Logger>
|
|
||||||
- exporter_ : std::unique_ptr<ExportService>
|
|
||||||
- brewery_context_strategy_ : std::unique_ptr<ContextStrategy>
|
|
||||||
- sampling_strategy_ : std::unique_ptr<SamplingStrategy>
|
|
||||||
- beer_selection_strategy_ : std::unique_ptr<BeerSelectionStrategy>
|
|
||||||
- checkin_strategy_ : std::unique_ptr<CheckinDistributionStrategy>
|
|
||||||
- follow_strategy_ : std::unique_ptr<FollowGenerationStrategy>
|
|
||||||
- beer_style_palette_ : std::vector<BeerStyle>
|
|
||||||
- options_ : ApplicationOptions
|
|
||||||
--
|
|
||||||
- user_pool_ : std::vector<GeneratedUser>
|
|
||||||
- brewery_pool_ : std::vector<GeneratedBrewery>
|
|
||||||
- beer_pool_ : std::vector<GeneratedBeer>
|
|
||||||
- checkin_pool_ : std::vector<GeneratedCheckin>
|
|
||||||
- follow_pool_ : std::vector<GeneratedFollow>
|
|
||||||
--
|
|
||||||
+ Run() : bool
|
|
||||||
- RunUserPhase(locations : const std::vector<Location>&) : void
|
|
||||||
- RunBreweryAndBeerPhase(locations : const std::vector<Location>&) : void
|
|
||||||
- RunCheckinPhase() : void
|
|
||||||
- RunRatingPhase() : void
|
|
||||||
- RunFollowPhase() : void
|
|
||||||
}
|
|
||||||
|
|
||||||
' --- Orchestration Aggregations (Services & Strategies) ---
|
' --- Orchestration Aggregations (Services & Strategies) ---
|
||||||
BiergartenPipelineOrchestrator *-- DataPreloader
|
BiergartenPipelineOrchestrator *-- DataPreloader
|
||||||
@@ -506,7 +514,6 @@ BiergartenPipelineOrchestrator *-- EnrichmentService
|
|||||||
BiergartenPipelineOrchestrator *-- DataGenerator
|
BiergartenPipelineOrchestrator *-- DataGenerator
|
||||||
BiergartenPipelineOrchestrator *-- ExportService
|
BiergartenPipelineOrchestrator *-- ExportService
|
||||||
BiergartenPipelineOrchestrator *-- CheckinDistributionStrategy
|
BiergartenPipelineOrchestrator *-- CheckinDistributionStrategy
|
||||||
BiergartenPipelineOrchestrator *-- FollowGenerationStrategy
|
|
||||||
BiergartenPipelineOrchestrator *-- SamplingStrategy
|
BiergartenPipelineOrchestrator *-- SamplingStrategy
|
||||||
BiergartenPipelineOrchestrator *-- BeerSelectionStrategy
|
BiergartenPipelineOrchestrator *-- BeerSelectionStrategy
|
||||||
BiergartenPipelineOrchestrator *-- ApplicationOptions
|
BiergartenPipelineOrchestrator *-- ApplicationOptions
|
||||||
@@ -517,7 +524,6 @@ BiergartenPipelineOrchestrator *-- "0..*" GeneratedUser : user_pool_
|
|||||||
BiergartenPipelineOrchestrator *-- "0..*" GeneratedBrewery : brewery_pool_
|
BiergartenPipelineOrchestrator *-- "0..*" GeneratedBrewery : brewery_pool_
|
||||||
BiergartenPipelineOrchestrator *-- "0..*" GeneratedBeer : beer_pool_
|
BiergartenPipelineOrchestrator *-- "0..*" GeneratedBeer : beer_pool_
|
||||||
BiergartenPipelineOrchestrator *-- "0..*" GeneratedCheckin : checkin_pool_
|
BiergartenPipelineOrchestrator *-- "0..*" GeneratedCheckin : checkin_pool_
|
||||||
BiergartenPipelineOrchestrator *-- "0..*" GeneratedFollow : follow_pool_
|
|
||||||
|
|
||||||
' --- Interfaces & Implementations ---
|
' --- Interfaces & Implementations ---
|
||||||
DataPreloader <|.. JsonLoader
|
DataPreloader <|.. JsonLoader
|
||||||
@@ -527,11 +533,8 @@ ContextStrategy <|.. BeerContextStrategy
|
|||||||
SamplingStrategy <|.. UniformSamplingStrategy
|
SamplingStrategy <|.. UniformSamplingStrategy
|
||||||
BeerSelectionStrategy <|.. RandomBeerSelectionStrategy
|
BeerSelectionStrategy <|.. RandomBeerSelectionStrategy
|
||||||
CheckinDistributionStrategy <|.. JCurveCheckinStrategy
|
CheckinDistributionStrategy <|.. JCurveCheckinStrategy
|
||||||
CheckinDistributionStrategy <|.. RandomCheckinStrategy
|
|
||||||
FollowGenerationStrategy <|.. RandomFollowStrategy
|
|
||||||
FollowGenerationStrategy <|.. ActivityWeightedFollowStrategy
|
|
||||||
EnrichmentService <|.. WikipediaService
|
EnrichmentService <|.. WikipediaService
|
||||||
WebClient <|.. HttpWebClient
|
WebClient <|.. CURLWebClient
|
||||||
DataGenerator <|.. MockGenerator
|
DataGenerator <|.. MockGenerator
|
||||||
DataGenerator <|.. LlamaGenerator
|
DataGenerator <|.. LlamaGenerator
|
||||||
PromptFormatter <|.. Gemma4JinjaPromptFormatter
|
PromptFormatter <|.. Gemma4JinjaPromptFormatter
|
||||||
@@ -542,7 +545,6 @@ DateTimeProvider <|.. SystemDateTimeProvider
|
|||||||
WikipediaService *-- WebClient
|
WikipediaService *-- WebClient
|
||||||
WikipediaService ..> ContextStrategy
|
WikipediaService ..> ContextStrategy
|
||||||
LlamaGenerator *-- PromptFormatter
|
LlamaGenerator *-- PromptFormatter
|
||||||
LlamaGenerator *-- IPromptDirectory
|
|
||||||
LlamaGenerator ..> GeneratorOptions
|
LlamaGenerator ..> GeneratorOptions
|
||||||
SqliteExportService *-- DateTimeProvider
|
SqliteExportService *-- DateTimeProvider
|
||||||
|
|
||||||
@@ -555,18 +557,12 @@ EnrichedCity *-- Location
|
|||||||
EnrichedCity *-- LocationContext
|
EnrichedCity *-- LocationContext
|
||||||
GeneratedBrewery *-- Location
|
GeneratedBrewery *-- Location
|
||||||
GeneratedBrewery *-- BreweryResult
|
GeneratedBrewery *-- BreweryResult
|
||||||
GeneratedBrewery *-- GenerationMetadata
|
|
||||||
GeneratedBeer *-- Location
|
GeneratedBeer *-- Location
|
||||||
GeneratedBeer *-- BeerStyle
|
GeneratedBeer *-- BeerStyle
|
||||||
GeneratedBeer *-- BeerResult
|
GeneratedBeer *-- BeerResult
|
||||||
GeneratedBeer *-- GenerationMetadata
|
|
||||||
GeneratedUser *-- Location
|
GeneratedUser *-- Location
|
||||||
GeneratedUser *-- UserResult
|
GeneratedUser *-- UserResult
|
||||||
GeneratedUser *-- GenerationMetadata
|
|
||||||
GeneratedCheckin *-- CheckinResult
|
GeneratedCheckin *-- CheckinResult
|
||||||
GeneratedCheckin *-- GenerationMetadata
|
|
||||||
GeneratedRating *-- RatingResult
|
GeneratedRating *-- RatingResult
|
||||||
GeneratedRating *-- GenerationMetadata
|
|
||||||
GeneratedFollow *-- GenerationMetadata
|
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
1
pipeline/diagrams/planned/output/biergarten_activity.svg
Normal file
1
pipeline/diagrams/planned/output/biergarten_activity.svg
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
83
pipeline/includes/biergarten_data_generator.h
Normal file
83
pipeline/includes/biergarten_data_generator.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_BIERGARTEN_DATA_GENERATOR_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_BIERGARTEN_DATA_GENERATOR_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file biergarten_data_generator.h
|
||||||
|
* @brief Core orchestration class for pipeline data generation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "data_generation/data_generator.h"
|
||||||
|
#include "data_model/enriched_city.h"
|
||||||
|
#include "data_model/generated_brewery.h"
|
||||||
|
#include "data_model/location.h"
|
||||||
|
#include "services/enrichment_service.h"
|
||||||
|
#include "services/export_service.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Main data generator class for the Biergarten pipeline.
|
||||||
|
*
|
||||||
|
* This class encapsulates the core logic for generating brewery data.
|
||||||
|
* It handles location loading, city enrichment, and brewery generation.
|
||||||
|
*/
|
||||||
|
class BiergartenDataGenerator {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Construct a BiergartenDataGenerator with injected dependencies.
|
||||||
|
*
|
||||||
|
* @param context_service Context provider for sampled locations.
|
||||||
|
* @param generator Brewery and user data generator.
|
||||||
|
* @param exporter Storage backend for generated brewery data.
|
||||||
|
*/
|
||||||
|
BiergartenDataGenerator(std::unique_ptr<IEnrichmentService> context_service,
|
||||||
|
std::unique_ptr<DataGenerator> generator,
|
||||||
|
std::unique_ptr<IExportService> exporter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Run the data generation pipeline.
|
||||||
|
*
|
||||||
|
* Performs the following steps:
|
||||||
|
* 1. Load curated locations from JSON
|
||||||
|
* 2. Resolve context for each city using the injected context service
|
||||||
|
* 3. Generate brewery data for sampled cities
|
||||||
|
*
|
||||||
|
* @return true if successful, false if not
|
||||||
|
*/
|
||||||
|
bool Run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// @brief Owning context provider dependency.
|
||||||
|
std::unique_ptr<IEnrichmentService> context_service_;
|
||||||
|
|
||||||
|
/// @brief Generator dependency selected in the composition root.
|
||||||
|
std::unique_ptr<DataGenerator> generator_;
|
||||||
|
|
||||||
|
/// @brief Storage backend for generated brewery records.
|
||||||
|
std::unique_ptr<IExportService> exporter_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load locations from JSON and sample cities.
|
||||||
|
*
|
||||||
|
* @return Vector of sampled locations capped at 50 entries.
|
||||||
|
*/
|
||||||
|
static std::vector<Location> QueryCitiesWithCountries();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generate breweries for enriched cities.
|
||||||
|
*
|
||||||
|
* @param cities Span of enriched city data.
|
||||||
|
*/
|
||||||
|
void GenerateBreweries(std::span<const EnrichedCity> cities);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Log the generated brewery results.
|
||||||
|
*/
|
||||||
|
void LogResults() const;
|
||||||
|
|
||||||
|
/// @brief Stores generated brewery data.
|
||||||
|
std::vector<GeneratedBrewery> generated_breweries_;
|
||||||
|
};
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_BIERGARTEN_DATA_GENERATOR_H_
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "data_model/generated_models.h"
|
#include "data_model/brewery_result.h"
|
||||||
|
#include "data_model/location.h"
|
||||||
|
#include "data_model/user_result.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Interface for data generator implementations.
|
* @brief Interface for data generator implementations.
|
||||||
@@ -14,11 +14,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include "../services/prompting/prompt_directory.h"
|
|
||||||
#include "data_generation/data_generator.h"
|
#include "data_generation/data_generator.h"
|
||||||
#include "data_generation/prompt_formatting/prompt_formatter.h"
|
#include "data_generation/prompt_formatting/prompt_formatter.h"
|
||||||
#include "data_model/models.h"
|
#include "data_model/application_options.h"
|
||||||
#include "services/logging/logger.h"
|
|
||||||
|
|
||||||
struct llama_model;
|
struct llama_model;
|
||||||
struct llama_context;
|
struct llama_context;
|
||||||
@@ -35,12 +33,10 @@ class LlamaGenerator final : public DataGenerator {
|
|||||||
* @param options Parsed application options.
|
* @param options Parsed application options.
|
||||||
* @param model_path Filesystem path to GGUF model assets.
|
* @param model_path Filesystem path to GGUF model assets.
|
||||||
* @param prompt_formatter Formatter that produces model-specific prompts.
|
* @param prompt_formatter Formatter that produces model-specific prompts.
|
||||||
* @param prompt_directory Directory service for loading named prompt files.
|
|
||||||
*/
|
*/
|
||||||
LlamaGenerator(const ApplicationOptions& options,
|
LlamaGenerator(const ApplicationOptions& options,
|
||||||
const std::string& model_path, std::shared_ptr<ILogger> logger,
|
const std::string& model_path,
|
||||||
std::unique_ptr<IPromptFormatter> prompt_formatter,
|
std::unique_ptr<IPromptFormatter> prompt_formatter);
|
||||||
std::unique_ptr<IPromptDirectory> prompt_directory);
|
|
||||||
|
|
||||||
~LlamaGenerator() override;
|
~LlamaGenerator() override;
|
||||||
|
|
||||||
@@ -123,6 +119,15 @@ class LlamaGenerator final : public DataGenerator {
|
|||||||
int max_tokens = kDefaultMaxTokens,
|
int max_tokens = kDefaultMaxTokens,
|
||||||
std::string_view grammar = {});
|
std::string_view grammar = {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Loads the brewery system prompt from disk.
|
||||||
|
*
|
||||||
|
* @param prompt_file_path Prompt file path to try first.
|
||||||
|
* @return Loaded prompt text.
|
||||||
|
*/
|
||||||
|
std::string LoadBrewerySystemPrompt(
|
||||||
|
const std::filesystem::path& prompt_file_path);
|
||||||
|
|
||||||
ModelHandle model_;
|
ModelHandle model_;
|
||||||
ContextHandle context_;
|
ContextHandle context_;
|
||||||
float sampling_temperature_ = 1.0F;
|
float sampling_temperature_ = 1.0F;
|
||||||
@@ -130,10 +135,8 @@ class LlamaGenerator final : public DataGenerator {
|
|||||||
uint32_t sampling_top_k_ = kDefaultSamplingTopK;
|
uint32_t sampling_top_k_ = kDefaultSamplingTopK;
|
||||||
std::mt19937 rng_;
|
std::mt19937 rng_;
|
||||||
uint32_t n_ctx_ = kDefaultContextSize;
|
uint32_t n_ctx_ = kDefaultContextSize;
|
||||||
int n_gpu_layers_ = 0;
|
std::string brewery_system_prompt_;
|
||||||
std::shared_ptr<ILogger> logger_;
|
|
||||||
std::unique_ptr<IPromptFormatter> prompt_formatter_;
|
std::unique_ptr<IPromptFormatter> prompt_formatter_;
|
||||||
std::unique_ptr<IPromptDirectory> prompt_directory_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_LLAMA_GENERATOR_H_
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_LLAMA_GENERATOR_H_
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include "data_model/generated_models.h"
|
#include "data_model/brewery_result.h"
|
||||||
|
|
||||||
struct llama_vocab;
|
struct llama_vocab;
|
||||||
using llama_token = int32_t;
|
using llama_token = int32_t;
|
||||||
@@ -44,13 +44,6 @@ class MockGenerator final : public DataGenerator {
|
|||||||
*/
|
*/
|
||||||
static size_t DeterministicHash(const Location& location);
|
static size_t DeterministicHash(const Location& location);
|
||||||
|
|
||||||
// Hash stride constants for deterministic distribution across fixed-size
|
|
||||||
// arrays. These coprime strides spread hash values uniformly without
|
|
||||||
// clustering, ensuring diverse output across different hash inputs.
|
|
||||||
static constexpr size_t kNounHashStride = 7;
|
|
||||||
static constexpr size_t kDescriptionHashStride = 13;
|
|
||||||
static constexpr size_t kBioHashStride = 11;
|
|
||||||
|
|
||||||
static constexpr std::array<std::string_view, 18> kBreweryAdjectives = {
|
static constexpr std::array<std::string_view, 18> kBreweryAdjectives = {
|
||||||
"Craft", "Heritage", "Local", "Artisan", "Pioneer", "Golden",
|
"Craft", "Heritage", "Local", "Artisan", "Pioneer", "Golden",
|
||||||
"Modern", "Classic", "Summit", "Northern", "Riverstone", "Barrel",
|
"Modern", "Classic", "Summit", "Northern", "Riverstone", "Barrel",
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_GEMMA4_JINJA_PROMPT_FORMATTER_H_
|
#pragma once
|
||||||
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_GEMMA4_JINJA_PROMPT_FORMATTER_H_
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@@ -14,5 +13,3 @@ class Gemma4JinjaPromptFormatter final : public IPromptFormatter {
|
|||||||
[[nodiscard]] std::string Format(std::string_view system_prompt,
|
[[nodiscard]] std::string Format(std::string_view system_prompt,
|
||||||
std::string_view user_prompt) const override;
|
std::string_view user_prompt) const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_GEMMA4_JINJA_PROMPT_FORMATTER_H_
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_PROMPT_FORMATTER_H_
|
#pragma once
|
||||||
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_PROMPT_FORMATTER_H_
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@@ -16,5 +15,3 @@ class IPromptFormatter {
|
|||||||
[[nodiscard]] virtual std::string Format(
|
[[nodiscard]] virtual std::string Format(
|
||||||
std::string_view system_prompt, std::string_view user_prompt) const = 0;
|
std::string_view system_prompt, std::string_view user_prompt) const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_PROMPT_FORMATTER_H_
|
|
||||||
42
pipeline/includes/data_model/application_options.h
Normal file
42
pipeline/includes/data_model/application_options.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_APPLICATION_OPTIONS_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_APPLICATION_OPTIONS_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/application_options.h
|
||||||
|
* @brief Program options for the Biergarten pipeline application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Program options for the Biergarten pipeline application.
|
||||||
|
*/
|
||||||
|
struct ApplicationOptions {
|
||||||
|
/// @brief Path to the LLM model file (gguf format); mutually exclusive with
|
||||||
|
/// use_mocked.
|
||||||
|
std::string model_path;
|
||||||
|
|
||||||
|
/// @brief Use mocked generator instead of LLM; mutually exclusive with
|
||||||
|
/// model_path.
|
||||||
|
bool use_mocked = false;
|
||||||
|
|
||||||
|
/// @brief LLM sampling temperature (0.0 to 1.0, higher = more random).
|
||||||
|
float temperature = 1.0F;
|
||||||
|
|
||||||
|
/// @brief LLM nucleus sampling top-p parameter (0.0 to 1.0, higher = more
|
||||||
|
/// random).
|
||||||
|
float top_p = 0.95F;
|
||||||
|
|
||||||
|
/// @brief LLM top-k sampling parameter.
|
||||||
|
uint32_t top_k = 64;
|
||||||
|
|
||||||
|
/// @brief Context window size (tokens) for LLM inference. Higher values
|
||||||
|
/// support longer prompts but use more memory.
|
||||||
|
uint32_t n_ctx = 8192;
|
||||||
|
|
||||||
|
/// @brief Random seed for sampling (-1 for random, otherwise non-negative).
|
||||||
|
int seed = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_APPLICATION_OPTIONS_H_
|
||||||
22
pipeline/includes/data_model/brewery_location.h
Normal file
22
pipeline/includes/data_model/brewery_location.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_LOCATION_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_LOCATION_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/brewery_location.h
|
||||||
|
* @brief Non-owning brewery location input.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Non-owning brewery location input.
|
||||||
|
*/
|
||||||
|
struct BreweryLocation {
|
||||||
|
/// @brief City name.
|
||||||
|
std::string_view city_name;
|
||||||
|
|
||||||
|
/// @brief Country name.
|
||||||
|
std::string_view country_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_LOCATION_H_
|
||||||
28
pipeline/includes/data_model/brewery_result.h
Normal file
28
pipeline/includes/data_model/brewery_result.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_RESULT_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_RESULT_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/brewery_result.h
|
||||||
|
* @brief Generated brewery payload.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generated brewery payload.
|
||||||
|
*/
|
||||||
|
struct BreweryResult {
|
||||||
|
/// @brief Brewery display name in English.
|
||||||
|
std::string name_en;
|
||||||
|
|
||||||
|
/// @brief Brewery description text in English.
|
||||||
|
std::string description_en;
|
||||||
|
|
||||||
|
/// @brief Brewery display name in the local language.
|
||||||
|
std::string name_local;
|
||||||
|
|
||||||
|
/// @brief Brewery description text in the local language.
|
||||||
|
std::string description_local;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_RESULT_H_
|
||||||
21
pipeline/includes/data_model/enriched_city.h
Normal file
21
pipeline/includes/data_model/enriched_city.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_ENRICHED_CITY_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_ENRICHED_CITY_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/enriched_city.h
|
||||||
|
* @brief Enriched city data with Wikipedia context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "data_model/location.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enriched city data with Wikipedia context.
|
||||||
|
*/
|
||||||
|
struct EnrichedCity {
|
||||||
|
Location location;
|
||||||
|
std::string region_context{};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_ENRICHED_CITY_H_
|
||||||
20
pipeline/includes/data_model/generated_brewery.h
Normal file
20
pipeline/includes/data_model/generated_brewery.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_BREWERY_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_BREWERY_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/generated_brewery.h
|
||||||
|
* @brief Helper struct to store generated brewery data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "data_model/brewery_result.h"
|
||||||
|
#include "data_model/location.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helper struct to store generated brewery data.
|
||||||
|
*/
|
||||||
|
struct GeneratedBrewery {
|
||||||
|
Location location;
|
||||||
|
BreweryResult brewery;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_BREWERY_H_
|
||||||
13
pipeline/includes/data_model/generation_models.h
Normal file
13
pipeline/includes/data_model/generation_models.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATION_MODELS_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATION_MODELS_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/generation_models.h
|
||||||
|
* @brief Convenience include for shared generation payload models.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "data_model/brewery_location.h"
|
||||||
|
#include "data_model/brewery_result.h"
|
||||||
|
#include "data_model/user_result.h"
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATION_MODELS_H_
|
||||||
41
pipeline/includes/data_model/location.h
Normal file
41
pipeline/includes/data_model/location.h
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_LOCATION_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_LOCATION_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/location.h
|
||||||
|
* @brief Location data model used throughout generation pipeline.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Canonical location record for city-level generation.
|
||||||
|
*/
|
||||||
|
struct Location {
|
||||||
|
/// @brief City name.
|
||||||
|
std::string city{};
|
||||||
|
|
||||||
|
/// @brief State or province name.
|
||||||
|
std::string state_province{};
|
||||||
|
|
||||||
|
/// @brief ISO 3166-2 subdivision code.
|
||||||
|
std::string iso3166_2{};
|
||||||
|
|
||||||
|
/// @brief Country name.
|
||||||
|
std::string country{};
|
||||||
|
|
||||||
|
/// @brief ISO 3166-1 country code.
|
||||||
|
std::string iso3166_1{};
|
||||||
|
|
||||||
|
/// @brief Local language codes in priority order.
|
||||||
|
std::vector<std::string> local_languages{};
|
||||||
|
|
||||||
|
/// @brief Latitude in decimal degrees.
|
||||||
|
double latitude{};
|
||||||
|
|
||||||
|
/// @brief Longitude in decimal degrees.
|
||||||
|
double longitude{};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_LOCATION_H_
|
||||||
12
pipeline/includes/data_model/pipeline_models.h
Normal file
12
pipeline/includes/data_model/pipeline_models.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_PIPELINE_MODELS_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_PIPELINE_MODELS_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/pipeline_models.h
|
||||||
|
* @brief Convenience include for pipeline-specific data models.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "data_model/enriched_city.h"
|
||||||
|
#include "data_model/generated_brewery.h"
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_PIPELINE_MODELS_H_
|
||||||
22
pipeline/includes/data_model/user_result.h
Normal file
22
pipeline/includes/data_model/user_result.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_USER_RESULT_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_USER_RESULT_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file data_model/user_result.h
|
||||||
|
* @brief Generated user profile payload.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generated user profile payload.
|
||||||
|
*/
|
||||||
|
struct UserResult {
|
||||||
|
/// @brief Username handle.
|
||||||
|
std::string username{};
|
||||||
|
|
||||||
|
/// @brief Short user biography.
|
||||||
|
std::string bio{};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_USER_RESULT_H_
|
||||||
@@ -7,19 +7,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "data_model/models.h"
|
#include "data_model/location.h"
|
||||||
#include "services/logging/logger.h"
|
|
||||||
|
|
||||||
/// @brief Loads curated world locations from a JSON file into memory.
|
/// @brief Loads curated world locations from a JSON file into memory.
|
||||||
class JsonLoader {
|
class JsonLoader {
|
||||||
public:
|
public:
|
||||||
/// @brief Parses a JSON array file and returns all location records.
|
/// @brief Parses a JSON array file and returns all location records.
|
||||||
static std::vector<Location> LoadLocations(
|
static std::vector<Location> LoadLocations(
|
||||||
const std::filesystem::path& filepath,
|
const std::filesystem::path& filepath);
|
||||||
std::shared_ptr<ILogger> logger = nullptr);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_JSON_HANDLING_JSON_LOADER_H_
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_JSON_HANDLING_JSON_LOADER_H_
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_DATE_TIME_PROVIDER_H_
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATE_TIME_PROVIDER_H_
|
||||||
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_DATE_TIME_PROVIDER_H_
|
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATE_TIME_PROVIDER_H_
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @file services/date_time_provider.h
|
* @file services/date_time_provider.h
|
||||||
@@ -63,4 +63,4 @@ class SystemDateTimeProvider final : public IDateTimeProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_DATE_TIME_PROVIDER_H_
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATE_TIME_PROVIDER_H_
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_ENRICHMENT_SERVICE_H_
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_SERVICE_H_
|
||||||
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_ENRICHMENT_SERVICE_H_
|
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_SERVICE_H_
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @file services/enrichment_service.h
|
* @file services/enrichment_service.h
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "data_model/models.h"
|
#include "data_model/location.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Interface for services that can enrich a location with context.
|
* @brief Interface for services that can enrich a location with context.
|
||||||
@@ -27,4 +27,4 @@ class IEnrichmentService {
|
|||||||
virtual std::string GetLocationContext(const Location& loc) = 0;
|
virtual std::string GetLocationContext(const Location& loc) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_ENRICHMENT_SERVICE_H_
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_SERVICE_H_
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_EXPORT_SERVICE_H_
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_EXPORT_SERVICE_H_
|
||||||
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_EXPORT_SERVICE_H_
|
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_EXPORT_SERVICE_H_
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @file services/export_service.h
|
* @file services/export_service.h
|
||||||
* @brief Abstraction for persisting generated brewery data.
|
* @brief Abstraction for persisting generated brewery data.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <cstdint>
|
#include "data_model/generated_brewery.h"
|
||||||
|
|
||||||
#include "data_model/generated_models.h"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Interface for services that persist generated brewery records.
|
* @brief Interface for services that persist generated brewery records.
|
||||||
@@ -33,10 +31,10 @@ class IExportService {
|
|||||||
*
|
*
|
||||||
* @param brewery Generated brewery payload to store.
|
* @param brewery Generated brewery payload to store.
|
||||||
*/
|
*/
|
||||||
virtual uint64_t ProcessRecord(const GeneratedBrewery& brewery) = 0;
|
virtual void ProcessRecord(const GeneratedBrewery& brewery) = 0;
|
||||||
|
|
||||||
/// @brief Finalizes the export destination.
|
/// @brief Finalizes the export destination.
|
||||||
virtual void Finalize() = 0;
|
virtual void Finalize() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_EXPORT_SERVICE_H_
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_EXPORT_SERVICE_H_
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_H_
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_H_
|
||||||
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_H_
|
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_H_
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @file services/sqlite_export_service.h
|
* @file services/sqlite_export_service.h
|
||||||
@@ -11,17 +11,16 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "data_model/models.h"
|
#include "services/date_time_provider.h"
|
||||||
#include "../datetime/date_time_provider.h"
|
#include "services/export_service.h"
|
||||||
#include "export_service.h"
|
#include "services/sqlite_export_service_helpers.h"
|
||||||
#include "sqlite_export_service_helpers.h"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Persists generated brewery records into a fresh SQLite database.
|
* @brief Persists generated brewery records into a fresh SQLite database.
|
||||||
*/
|
*/
|
||||||
class SqliteExportService final : public IExportService {
|
class SqliteExportService final : public IExportService {
|
||||||
public:
|
public:
|
||||||
explicit SqliteExportService(const ApplicationOptions& options);
|
SqliteExportService();
|
||||||
~SqliteExportService() override;
|
~SqliteExportService() override;
|
||||||
|
|
||||||
SqliteExportService(const SqliteExportService&) = delete;
|
SqliteExportService(const SqliteExportService&) = delete;
|
||||||
@@ -30,7 +29,7 @@ class SqliteExportService final : public IExportService {
|
|||||||
SqliteExportService& operator=(SqliteExportService&&) = delete;
|
SqliteExportService& operator=(SqliteExportService&&) = delete;
|
||||||
|
|
||||||
void Initialize() override;
|
void Initialize() override;
|
||||||
uint64_t ProcessRecord(const GeneratedBrewery& brewery) override;
|
void ProcessRecord(const GeneratedBrewery& brewery) override;
|
||||||
void Finalize() override;
|
void Finalize() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -39,15 +38,15 @@ class SqliteExportService final : public IExportService {
|
|||||||
using SqliteStatementHandle =
|
using SqliteStatementHandle =
|
||||||
sqlite_export_service_internal::SqliteStatementHandle;
|
sqlite_export_service_internal::SqliteStatementHandle;
|
||||||
|
|
||||||
void InitializeSchema() const;
|
void InitializeSchema();
|
||||||
void PrepareStatements();
|
void PrepareStatements();
|
||||||
void RollbackAndCloseNoThrow() noexcept;
|
void RollbackAndCloseNoThrow() noexcept;
|
||||||
|
void FinalizeStatements() noexcept;
|
||||||
|
|
||||||
[[nodiscard]] std::filesystem::path BuildDatabasePath() const;
|
[[nodiscard]] std::filesystem::path BuildDatabasePath() const;
|
||||||
[[nodiscard]] static std::string BuildLocationKey(const Location& location);
|
[[nodiscard]] static std::string BuildLocationKey(const Location& location);
|
||||||
|
|
||||||
std::unique_ptr<IDateTimeProvider> date_time_provider_;
|
std::unique_ptr<IDateTimeProvider> date_time_provider_;
|
||||||
std::filesystem::path output_path_;
|
|
||||||
std::string run_timestamp_utc_;
|
std::string run_timestamp_utc_;
|
||||||
std::filesystem::path database_path_;
|
std::filesystem::path database_path_;
|
||||||
SqliteDatabaseHandle db_handle_;
|
SqliteDatabaseHandle db_handle_;
|
||||||
@@ -57,4 +56,4 @@ class SqliteExportService final : public IExportService {
|
|||||||
std::unordered_map<std::string, sqlite3_int64> location_cache_;
|
std::unordered_map<std::string, sqlite3_int64> location_cache_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_H_
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_H_
|
||||||
250
pipeline/includes/services/sqlite_export_service_helpers.h
Normal file
250
pipeline/includes/services/sqlite_export_service_helpers.h
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_HELPERS_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_HELPERS_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file services/sqlite_export_service_helpers.h
|
||||||
|
* @brief Internal SQLite export helpers shared across per-method translation
|
||||||
|
* units.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace sqlite_export_service_internal {
|
||||||
|
|
||||||
|
struct SqliteDatabaseDeleter {
|
||||||
|
void operator()(sqlite3* handle) const noexcept {
|
||||||
|
if (handle != nullptr) {
|
||||||
|
sqlite3_close(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SqliteStatementDeleter {
|
||||||
|
void operator()(sqlite3_stmt* statement) const noexcept {
|
||||||
|
if (statement != nullptr) {
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using SqliteDatabaseHandle = std::unique_ptr<sqlite3, SqliteDatabaseDeleter>;
|
||||||
|
using SqliteStatementHandle =
|
||||||
|
std::unique_ptr<sqlite3_stmt, SqliteStatementDeleter>;
|
||||||
|
|
||||||
|
inline constexpr std::string_view kCreateLocationsTableSql = R"sql(
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS locations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
city TEXT NOT NULL,
|
||||||
|
state_province TEXT NOT NULL,
|
||||||
|
iso3166_2 TEXT NOT NULL,
|
||||||
|
country TEXT NOT NULL,
|
||||||
|
iso3166_1 TEXT NOT NULL,
|
||||||
|
local_languages_json TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
UNIQUE(city, state_province, iso3166_2, country, latitude, longitude)
|
||||||
|
);
|
||||||
|
|
||||||
|
)sql";
|
||||||
|
|
||||||
|
inline constexpr std::string_view kCreateBreweriesTableSql = R"sql(
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS breweries (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
location_id INTEGER NOT NULL,
|
||||||
|
name_en TEXT NOT NULL,
|
||||||
|
description_en TEXT NOT NULL,
|
||||||
|
name_local TEXT NOT NULL,
|
||||||
|
description_local TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(location_id) REFERENCES locations(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_breweries_location_id ON breweries(location_id);
|
||||||
|
|
||||||
|
)sql";
|
||||||
|
|
||||||
|
inline constexpr std::string_view kInsertLocationSql = R"sql(
|
||||||
|
INSERT INTO locations (
|
||||||
|
city,
|
||||||
|
state_province,
|
||||||
|
iso3166_2,
|
||||||
|
country,
|
||||||
|
iso3166_1,
|
||||||
|
local_languages_json,
|
||||||
|
latitude,
|
||||||
|
longitude
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
)sql";
|
||||||
|
|
||||||
|
inline constexpr std::string_view kInsertBrewerySql = R"sql(
|
||||||
|
INSERT INTO breweries (
|
||||||
|
location_id,
|
||||||
|
name_en,
|
||||||
|
description_en,
|
||||||
|
name_local,
|
||||||
|
description_local
|
||||||
|
) VALUES (?, ?, ?, ?, ?);
|
||||||
|
)sql";
|
||||||
|
|
||||||
|
inline constexpr int kLocationCityBindIndex = 1;
|
||||||
|
inline constexpr int kLocationStateProvinceBindIndex = 2;
|
||||||
|
inline constexpr int kLocationIso31662BindIndex = 3;
|
||||||
|
inline constexpr int kLocationCountryBindIndex = 4;
|
||||||
|
inline constexpr int kLocationIso31661BindIndex = 5;
|
||||||
|
inline constexpr int kLocationLanguagesBindIndex = 6;
|
||||||
|
inline constexpr int kLocationLatitudeBindIndex = 7;
|
||||||
|
inline constexpr int kLocationLongitudeBindIndex = 8;
|
||||||
|
|
||||||
|
inline constexpr int kBreweryLocationIdBindIndex = 1;
|
||||||
|
inline constexpr int kBreweryEnglishNameBindIndex = 2;
|
||||||
|
inline constexpr int kBreweryEnglishDescriptionBindIndex = 3;
|
||||||
|
inline constexpr int kBreweryLocalNameBindIndex = 4;
|
||||||
|
inline constexpr int kBreweryLocalDescriptionBindIndex = 5;
|
||||||
|
|
||||||
|
inline void ThrowSqliteError(sqlite3* db_handle, std::string_view action) {
|
||||||
|
const std::string message =
|
||||||
|
db_handle != nullptr ? sqlite3_errmsg(db_handle) : "unknown SQLite error";
|
||||||
|
throw std::runtime_error(std::string(action) + ": " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline SqliteDatabaseHandle OpenDatabase(const std::filesystem::path& path) {
|
||||||
|
sqlite3* raw_handle = nullptr;
|
||||||
|
const std::string path_string = path.string();
|
||||||
|
const int result = sqlite3_open(path_string.c_str(), &raw_handle);
|
||||||
|
SqliteDatabaseHandle handle(raw_handle);
|
||||||
|
if (result != SQLITE_OK) {
|
||||||
|
const std::string message = raw_handle != nullptr
|
||||||
|
? sqlite3_errmsg(raw_handle)
|
||||||
|
: "unknown SQLite error";
|
||||||
|
throw std::runtime_error("Failed to open SQLite export database: " +
|
||||||
|
message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void ExecSql(const SqliteDatabaseHandle& db_handle, std::string_view sql,
|
||||||
|
const char* action) {
|
||||||
|
char* error_message = nullptr;
|
||||||
|
const std::string sql_text(sql);
|
||||||
|
const int result = sqlite3_exec(db_handle.get(), sql_text.c_str(), nullptr,
|
||||||
|
nullptr, &error_message);
|
||||||
|
if (result != SQLITE_OK) {
|
||||||
|
const std::string message = error_message != nullptr
|
||||||
|
? error_message
|
||||||
|
: sqlite3_errmsg(db_handle.get());
|
||||||
|
sqlite3_free(error_message);
|
||||||
|
throw std::runtime_error(std::string(action) + ": " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline SqliteStatementHandle PrepareStatement(
|
||||||
|
const SqliteDatabaseHandle& db_handle, std::string_view sql,
|
||||||
|
const char* action) {
|
||||||
|
sqlite3_stmt* raw_statement = nullptr;
|
||||||
|
const std::string sql_text(sql);
|
||||||
|
const int result = sqlite3_prepare_v2(db_handle.get(), sql_text.c_str(), -1,
|
||||||
|
&raw_statement, nullptr);
|
||||||
|
SqliteStatementHandle statement(raw_statement);
|
||||||
|
if (result != SQLITE_OK) {
|
||||||
|
ThrowSqliteError(db_handle.get(), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void ResetStatement(SqliteStatementHandle& statement) {
|
||||||
|
if (statement != nullptr) {
|
||||||
|
sqlite3_reset(statement.get());
|
||||||
|
sqlite3_clear_bindings(statement.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void DeleteCharArray(void* data) noexcept {
|
||||||
|
delete[] static_cast<char*>(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void BindText(const SqliteStatementHandle& statement, int index,
|
||||||
|
std::string_view value, const char* action) {
|
||||||
|
const auto byte_count = value.size();
|
||||||
|
if (byte_count > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||||
|
ThrowSqliteError(sqlite3_db_handle(statement.get()), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto buffer = std::make_unique<char[]>(byte_count + 1);
|
||||||
|
std::memcpy(buffer.get(), value.data(), byte_count);
|
||||||
|
buffer[byte_count] = '\0';
|
||||||
|
|
||||||
|
char* raw_buffer = buffer.release();
|
||||||
|
const int result =
|
||||||
|
sqlite3_bind_text(statement.get(), index, raw_buffer,
|
||||||
|
static_cast<int>(byte_count), DeleteCharArray);
|
||||||
|
if (result != SQLITE_OK) {
|
||||||
|
DeleteCharArray(raw_buffer);
|
||||||
|
ThrowSqliteError(sqlite3_db_handle(statement.get()), action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void BindDouble(const SqliteStatementHandle& statement, int index,
|
||||||
|
double value, std::string_view action) {
|
||||||
|
const int result = sqlite3_bind_double(statement.get(), index, value);
|
||||||
|
if (result != SQLITE_OK) {
|
||||||
|
ThrowSqliteError(sqlite3_db_handle(statement.get()), action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void BindInt64(const SqliteStatementHandle& statement, int index,
|
||||||
|
sqlite3_int64 value, std::string_view action) {
|
||||||
|
const int result = sqlite3_bind_int64(statement.get(), index, value);
|
||||||
|
if (result != SQLITE_OK) {
|
||||||
|
ThrowSqliteError(sqlite3_db_handle(statement.get()), action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void StepStatement(const SqliteDatabaseHandle& db_handle,
|
||||||
|
const SqliteStatementHandle& statement,
|
||||||
|
std::string_view action) {
|
||||||
|
const int result = sqlite3_step(statement.get());
|
||||||
|
if (result != SQLITE_DONE) {
|
||||||
|
ThrowSqliteError(db_handle.get(), action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline sqlite3_int64 LastInsertRowId(const SqliteDatabaseHandle& db_handle) {
|
||||||
|
return sqlite3_last_insert_rowid(db_handle.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void RollbackTransactionNoThrow(
|
||||||
|
const SqliteDatabaseHandle& db_handle) noexcept {
|
||||||
|
if (!db_handle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_exec(db_handle.get(), "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string SerializeLocalLanguages(
|
||||||
|
const std::vector<std::string>& local_languages) {
|
||||||
|
boost::json::array array;
|
||||||
|
array.reserve(local_languages.size());
|
||||||
|
for (const auto& language : local_languages) {
|
||||||
|
array.emplace_back(language);
|
||||||
|
}
|
||||||
|
return boost::json::serialize(array);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sqlite_export_service_internal
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_HELPERS_H_
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_WIKIPEDIA_SERVICE_H_
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_WIKIPEDIA_SERVICE_H_
|
||||||
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_WIKIPEDIA_SERVICE_H_
|
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_WIKIPEDIA_SERVICE_H_
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @file services/wikipedia_service.h
|
* @file services/wikipedia_service.h
|
||||||
@@ -11,16 +11,14 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "enrichment_service.h"
|
#include "services/enrichment_service.h"
|
||||||
#include "services/logging/logger.h"
|
|
||||||
#include "web_client/web_client.h"
|
#include "web_client/web_client.h"
|
||||||
|
|
||||||
/// @brief Provides Wikipedia summary lookups backed by cached raw extracts.
|
/// @brief Provides Wikipedia summary lookups backed by cached raw extracts.
|
||||||
class WikipediaEnrichmentService final : public IEnrichmentService {
|
class WikipediaService final : public IEnrichmentService {
|
||||||
public:
|
public:
|
||||||
/// @brief Creates a new Wikipedia service with the provided web client.
|
/// @brief Creates a new Wikipedia service with the provided web client.
|
||||||
explicit WikipediaEnrichmentService(std::unique_ptr<WebClient> client,
|
explicit WikipediaService(std::unique_ptr<WebClient> client);
|
||||||
std::shared_ptr<ILogger> logger);
|
|
||||||
|
|
||||||
/// @brief Returns the Wikipedia-derived context for a location.
|
/// @brief Returns the Wikipedia-derived context for a location.
|
||||||
[[nodiscard]] std::string GetLocationContext(const Location& loc) override;
|
[[nodiscard]] std::string GetLocationContext(const Location& loc) override;
|
||||||
@@ -28,9 +26,8 @@ class WikipediaEnrichmentService final : public IEnrichmentService {
|
|||||||
private:
|
private:
|
||||||
std::string FetchExtract(std::string_view query);
|
std::string FetchExtract(std::string_view query);
|
||||||
std::unique_ptr<WebClient> client_;
|
std::unique_ptr<WebClient> client_;
|
||||||
std::shared_ptr<ILogger> logger_;
|
|
||||||
/// @brief Canonical cache for raw Wikipedia query extracts.
|
/// @brief Canonical cache for raw Wikipedia query extracts.
|
||||||
std::unordered_map<std::string, std::string> extract_cache_;
|
std::unordered_map<std::string, std::string> extract_cache_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_WIKIPEDIA_SERVICE_H_
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_WIKIPEDIA_SERVICE_H_
|
||||||
54
pipeline/includes/web_client/curl_web_client.h
Normal file
54
pipeline/includes/web_client/curl_web_client.h
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#ifndef BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_CURL_WEB_CLIENT_H_
|
||||||
|
#define BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_CURL_WEB_CLIENT_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file web_client/curl_web_client.h
|
||||||
|
* @brief libcurl-based WebClient implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "web_client/web_client.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RAII wrapper for curl_global_init and curl_global_cleanup.
|
||||||
|
*
|
||||||
|
* Create one instance in application startup before using libcurl and keep it
|
||||||
|
* alive for application lifetime.
|
||||||
|
*/
|
||||||
|
class CurlGlobalState {
|
||||||
|
public:
|
||||||
|
/// @brief Initializes global libcurl state.
|
||||||
|
CurlGlobalState();
|
||||||
|
|
||||||
|
/// @brief Cleans up global libcurl state.
|
||||||
|
~CurlGlobalState();
|
||||||
|
|
||||||
|
/// @brief Non-copyable type.
|
||||||
|
CurlGlobalState(const CurlGlobalState&) = delete;
|
||||||
|
|
||||||
|
/// @brief Non-copyable type.
|
||||||
|
CurlGlobalState& operator=(const CurlGlobalState&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief WebClient implementation backed by libcurl.
|
||||||
|
*/
|
||||||
|
class CURLWebClient : public WebClient {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Executes an HTTP GET request.
|
||||||
|
*
|
||||||
|
* @param url Request URL.
|
||||||
|
* @return Response body.
|
||||||
|
*/
|
||||||
|
std::string Get(const std::string& url) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief URL-encodes a string value.
|
||||||
|
*
|
||||||
|
* @param value Raw value.
|
||||||
|
* @return URL-encoded string.
|
||||||
|
*/
|
||||||
|
std::string UrlEncode(const std::string& value) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_CURL_WEB_CLIENT_H_
|
||||||
@@ -30,7 +30,7 @@ class WebClient {
|
|||||||
* @param value Raw string value.
|
* @param value Raw string value.
|
||||||
* @return Encoded value safe for URL usage.
|
* @return Encoded value safe for URL usage.
|
||||||
*/
|
*/
|
||||||
virtual std::string EncodeURL(const std::string& value) = 0;
|
virtual std::string UrlEncode(const std::string& value) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_WEB_CLIENT_H_
|
#endif // BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_WEB_CLIENT_H_
|
||||||
113
pipeline/prompts/system.md
Normal file
113
pipeline/prompts/system.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# FULL SYSTEM PROMPT
|
||||||
|
|
||||||
|
You are an expert brewery copywriter, an architectural observer, and a master of zymurgy.
|
||||||
|
|
||||||
|
Your main goal is to come up with a fake, contextually accurate name and a matching description for a craft brewery located in a specific city. You need to base this on the exact geographic and cultural info provided. You also need to seamlessly blend historical background, cultural details, and highly specialized brewing methods to create a realistic and interesting story.
|
||||||
|
|
||||||
|
You will receive the inputs like this:
|
||||||
|
|
||||||
|
## CITY:
|
||||||
|
|
||||||
|
[City Name]
|
||||||
|
|
||||||
|
## COUNTRY:
|
||||||
|
|
||||||
|
[Country Name]
|
||||||
|
|
||||||
|
## LOCAL LANGUAGE CODES:
|
||||||
|
|
||||||
|
[Local language codes in priority order]
|
||||||
|
|
||||||
|
## CONTEXT:
|
||||||
|
|
||||||
|
[Information about local beer culture, history, geography, or language context]
|
||||||
|
|
||||||
|
## CRITICAL OUTPUT FORMAT (READ CAREFULLY):
|
||||||
|
|
||||||
|
ABSOLUTELY NO MARKDOWN FORMATTING. Do NOT wrap your response in json or ``` blocks.
|
||||||
|
|
||||||
|
Do not add markdown, code fences, or postscript around the final JSON object. Do not say "Here is the JSON" or "Enjoy!".
|
||||||
|
|
||||||
|
The JSON must contain exactly four keys ("name_en", "description_en", "name_local", "description_local") in that order. Do not rename or add any other keys.
|
||||||
|
|
||||||
|
ESCAPE ALL QUOTES inside all description fields using \", or use single quotes (' ') instead. This applies equally to description_en and description_local. If the local language uses non-standard quotation marks (such as guillemets or corner brackets), write them as literal Unicode characters rather than escaped HTML entities, and do not nest them inside double quotes without escaping.
|
||||||
|
|
||||||
|
DO NOT use actual line breaks (\n) inside any string. Keep all descriptions as one continuous string each.
|
||||||
|
|
||||||
|
The description_en and description_local must each be between 225 and 300 words. Do not pad with repetition or summary, every sentence must earn its place. Be concise and specific.
|
||||||
|
|
||||||
|
Expected JSON format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name_en": "Fictional Local Brewery Name in English",
|
||||||
|
"description_en": "The English description goes here.",
|
||||||
|
"name_local": "Translated brewery name in the local language",
|
||||||
|
"description_local": "The localised description goes here."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CONTENT RULES AND CONSTRAINTS:
|
||||||
|
|
||||||
|
### THE HOOK:
|
||||||
|
|
||||||
|
The first sentence must be a sensory environmental hook written as a personal observation, something the owner notices or has always noticed. It should establish the local weather, smell, or soundscape of the city. Do not open with the brewery's name or a generic welcome.
|
||||||
|
|
||||||
|
### GEOGRAPHIC & CULTURAL ANCHOR:
|
||||||
|
|
||||||
|
The story must be deeply tied to the provided geographic and cultural info. Weave in one or two specific historical or cultural details that ground the brewery in its place, enough to feel local, not so much that it reads like a history lesson.
|
||||||
|
|
||||||
|
### TECHNICAL BREWING DETAIL (VARY THIS!):
|
||||||
|
|
||||||
|
You must include one highly specialized technical brewing detail. To avoid sounding repetitive, make sure this varies a lot. Some examples: using local wild yeast (like spontaneous Brettanomyces), adjusting the water profile (like Burtonization), specific mashing techniques, or using local barrels for aging. Don't use basic concepts like generic mash temperatures.
|
||||||
|
|
||||||
|
### ARCHITECTURAL DETAIL (VARY THIS!):
|
||||||
|
|
||||||
|
You must include one specific architectural or environmental detail, highlighting the building's physical wear, structure, or history. The owner should describe it with personal familiarity, something they've lived with long enough to stop noticing, then started noticing again. Avoid overused industry clichés like repurposed dairy equipment or glycol chillers.
|
||||||
|
|
||||||
|
### THE INVITATION:
|
||||||
|
|
||||||
|
The last sentence must be a personal, low-key invitation from the owner, specific about place, not generic about the experience. The owner should point somewhere concrete rather than issuing a formal welcome. Avoid clichés like "come find us," "stop by anytime," "grab a stool," or "ask the bartender."
|
||||||
|
|
||||||
|
### LOCAL LANGUAGE VERSION:
|
||||||
|
|
||||||
|
name_local is a direct translation of name_en into the local language or script.
|
||||||
|
Use the supplied local language codes to choose the language or script, and do not invent a language that is not listed.
|
||||||
|
|
||||||
|
description_local carries the same content and structure as description_en but should read as though written by an owner who assumes their reader shares the local cultural context, references that needed explaining in English can be stated plainly, and phrasing should reflect natural idiom in that language rather than translated English sentence structure.
|
||||||
|
|
||||||
|
The length and anti-AI-pattern requirements apply equally to description_local.
|
||||||
|
|
||||||
|
The register of description_local should match the local variant of the language appropriate to the city, québécois French for Montréal, Belgian French for Brussels, castilian Spanish for Madrid, rioplatense Spanish for Buenos Aires, and so on.
|
||||||
|
|
||||||
|
### THE BLOCKLIST (FORBIDDEN CONCEPTS):
|
||||||
|
|
||||||
|
You absolutely cannot use the following words and phrases. Make sure your final output doesn't have any of these:
|
||||||
|
|
||||||
|
- "hidden gem"
|
||||||
|
- "passion"
|
||||||
|
- "authentic"
|
||||||
|
- "repurposed dairy tank"
|
||||||
|
- "repurposed industrial vat"
|
||||||
|
- "concrete eggs"
|
||||||
|
- "glycol chiller"
|
||||||
|
- "mash temperature"
|
||||||
|
- "grab a stool"
|
||||||
|
- "ask the bartender"
|
||||||
|
- "come find us"
|
||||||
|
- "stop by anytime"
|
||||||
|
|
||||||
|
#### FORBIDDEN WRITING PATTERNS
|
||||||
|
|
||||||
|
The following patterns are common AI writing pitfalls and must not appear in either description:
|
||||||
|
|
||||||
|
- Negative parallelism constructions: "It's not X, it's Y" or "We're not about X, we're about Y"
|
||||||
|
- Inflated significance phrases: "stands as a testament," "plays a vital role," "leaves a lasting impact," "watershed moment," "deeply rooted," "rich cultural heritage," "rich cultural tapestry," "enduring legacy"
|
||||||
|
- Superficial trailing analyses: sentences ending in -ing words that add opinion without content ("ensuring consistency," "reflecting the city's spirit," "highlighting our commitment")
|
||||||
|
- Promotional travel-copy tone: "breathtaking," "must-visit," "stunning," "vibrant"
|
||||||
|
- Overused conjunctive transitions used as sentence openers: "Moreover," "Furthermore," "In addition," "In contrast"
|
||||||
|
- Rule of three: do not consistently organise ideas or examples in triplets
|
||||||
|
|
||||||
|
### VOICE & PERSPECTIVE:
|
||||||
|
|
||||||
|
The description must be written in the first person, from the perspective of the brewery's owner. Favour "we" and "our" over "I" and "my." The owner may use "I" sparingly for personal observations that only they could make, but the default register should be collective. The tone should feel lived-in and a little weathered. Do not use third-person or second-person pronouns.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* @file biergarten_data_generator/biergarten_data_generator.cc
|
||||||
|
* @brief BiergartenDataGenerator constructor implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "biergarten_data_generator.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
BiergartenDataGenerator::BiergartenDataGenerator(
|
||||||
|
std::unique_ptr<IEnrichmentService> context_service,
|
||||||
|
std::unique_ptr<DataGenerator> generator,
|
||||||
|
std::unique_ptr<IExportService> exporter)
|
||||||
|
: context_service_(std::move(context_service)),
|
||||||
|
generator_(std::move(generator)),
|
||||||
|
exporter_(std::move(exporter)) {}
|
||||||
58
pipeline/src/biergarten_data_generator/generate_breweries.cc
Normal file
58
pipeline/src/biergarten_data_generator/generate_breweries.cc
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* @file biergarten_data_generator/generate_breweries.cc
|
||||||
|
* @brief BiergartenDataGenerator::GenerateBreweries() implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include "biergarten_data_generator.h"
|
||||||
|
|
||||||
|
void BiergartenDataGenerator::GenerateBreweries(
|
||||||
|
std::span<const EnrichedCity> cities) {
|
||||||
|
spdlog::info("\n=== SAMPLE BREWERY GENERATION ===");
|
||||||
|
|
||||||
|
generated_breweries_.clear();
|
||||||
|
size_t skipped_count = 0;
|
||||||
|
size_t export_failed_count = 0;
|
||||||
|
|
||||||
|
for (const auto& [location, region_context] : cities) {
|
||||||
|
try {
|
||||||
|
const BreweryResult brewery =
|
||||||
|
generator_->GenerateBrewery(location, region_context);
|
||||||
|
|
||||||
|
const GeneratedBrewery gen{.location = location, .brewery = brewery};
|
||||||
|
|
||||||
|
generated_breweries_.push_back(gen);
|
||||||
|
|
||||||
|
try {
|
||||||
|
exporter_->ProcessRecord(gen);
|
||||||
|
} catch (const std::exception& export_exception) {
|
||||||
|
++export_failed_count;
|
||||||
|
|
||||||
|
spdlog::warn(
|
||||||
|
"[Pipeline] Generated brewery for '{}' ({}) but SQLite export "
|
||||||
|
"failed: {}",
|
||||||
|
location.city, location.country, export_exception.what());
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
++skipped_count;
|
||||||
|
|
||||||
|
spdlog::warn(
|
||||||
|
"[Pipeline] Skipping city '{}' ({}): brewery generation failed: "
|
||||||
|
"{}",
|
||||||
|
location.city, location.country, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipped_count > 0) {
|
||||||
|
spdlog::warn("[Pipeline] Skipped {} city/cities due to generation errors",
|
||||||
|
skipped_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (export_failed_count > 0) {
|
||||||
|
spdlog::warn(
|
||||||
|
"[Pipeline] Failed to export {} generated brewery/breweries to "
|
||||||
|
"SQLite",
|
||||||
|
export_failed_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
pipeline/src/biergarten_data_generator/log_results.cc
Normal file
26
pipeline/src/biergarten_data_generator/log_results.cc
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* @file biergarten_data_generator/log_results.cc
|
||||||
|
* @brief BiergartenDataGenerator::LogResults() implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include "biergarten_data_generator.h"
|
||||||
|
|
||||||
|
void BiergartenDataGenerator::LogResults() const {
|
||||||
|
spdlog::info("\n=== GENERATED DATA DUMP ===");
|
||||||
|
size_t index = 1;
|
||||||
|
for (const auto& [location, brewery] : generated_breweries_) {
|
||||||
|
spdlog::info(
|
||||||
|
"{}. city=\"{}\" country=\"{}\" state=\"{}\" "
|
||||||
|
"iso3166_2={} lat={} lon={}",
|
||||||
|
index, location.city, location.country, location.state_province,
|
||||||
|
location.iso3166_2, location.latitude, location.longitude);
|
||||||
|
spdlog::info(" brewery_name_en=\"{}\"", brewery.name_en);
|
||||||
|
spdlog::info(" brewery_description_en=\"{}\"", brewery.description_en);
|
||||||
|
spdlog::info(" brewery_name_local=\"{}\"", brewery.name_local);
|
||||||
|
spdlog::info(" brewery_description_local=\"{}\"",
|
||||||
|
brewery.description_local);
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @file biergarten_data_generator/query_cities_with_countries.cc
|
||||||
|
* @brief BiergartenDataGenerator::QueryCitiesWithCountries() implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iterator>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include "biergarten_data_generator.h"
|
||||||
|
#include "json_handling/json_loader.h"
|
||||||
|
|
||||||
|
static constexpr size_t kBreweryAmount = 50;
|
||||||
|
|
||||||
|
std::vector<Location> BiergartenDataGenerator::QueryCitiesWithCountries() {
|
||||||
|
spdlog::info("\n=== GEOGRAPHIC DATA OVERVIEW ===");
|
||||||
|
|
||||||
|
const std::filesystem::path locations_path = "locations.json";
|
||||||
|
|
||||||
|
auto all_locations = JsonLoader::LoadLocations(locations_path);
|
||||||
|
spdlog::info(" Locations available: {}", all_locations.size());
|
||||||
|
|
||||||
|
const size_t sample_count = std::min(kBreweryAmount, all_locations.size());
|
||||||
|
|
||||||
|
const auto sample_count_signed =
|
||||||
|
static_cast<std::iter_difference_t<decltype(all_locations.cbegin())>>(
|
||||||
|
sample_count);
|
||||||
|
|
||||||
|
std::vector<Location> sampled_locations;
|
||||||
|
sampled_locations.reserve(sample_count);
|
||||||
|
|
||||||
|
std::random_device random_generator;
|
||||||
|
std::ranges::sample(all_locations, std::back_inserter(sampled_locations),
|
||||||
|
sample_count_signed, random_generator);
|
||||||
|
|
||||||
|
spdlog::info(" Sampled locations: {}", sampled_locations.size());
|
||||||
|
return sampled_locations;
|
||||||
|
}
|
||||||
52
pipeline/src/biergarten_data_generator/run.cc
Normal file
52
pipeline/src/biergarten_data_generator/run.cc
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* @file biergarten_data_generator/run.cc
|
||||||
|
* @brief BiergartenDataGenerator::Run() implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "biergarten_data_generator.h"
|
||||||
|
|
||||||
|
bool BiergartenDataGenerator::Run() {
|
||||||
|
try {
|
||||||
|
exporter_->Initialize();
|
||||||
|
|
||||||
|
std::vector<Location> cities = QueryCitiesWithCountries();
|
||||||
|
std::vector<EnrichedCity> enriched;
|
||||||
|
enriched.reserve(cities.size());
|
||||||
|
|
||||||
|
size_t skipped_count = 0;
|
||||||
|
for (auto& city : cities) {
|
||||||
|
try {
|
||||||
|
std::string region_context = context_service_->GetLocationContext(city);
|
||||||
|
spdlog::debug("[Pipeline] Context for '{}' ({}) gathered:\n{}",
|
||||||
|
city.city, city.country, region_context);
|
||||||
|
|
||||||
|
enriched.push_back(
|
||||||
|
EnrichedCity{.location = std::move(city),
|
||||||
|
.region_context = std::move(region_context)});
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
++skipped_count;
|
||||||
|
spdlog::warn(
|
||||||
|
"[Pipeline] Skipping city '{}' ({}): context lookup failed: {}",
|
||||||
|
city.city, city.country, exception.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipped_count > 0) {
|
||||||
|
spdlog::warn(
|
||||||
|
"[Pipeline] Skipped {} city/cities due to context lookup errors",
|
||||||
|
skipped_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->GenerateBreweries(enriched);
|
||||||
|
exporter_->Finalize();
|
||||||
|
this->LogResults();
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
spdlog::error("Pipeline execution failed with error: {}", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
* inference, and validates structured JSON output for brewery records.
|
* inference, and validates structured JSON output for brewery records.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <chrono>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
@@ -32,9 +33,6 @@ static std::string FormatLocalLanguageCodes(
|
|||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GBNF grammar for structured brewery JSON output.
|
|
||||||
// @TODO move to a separate gbnf file if it grows in complexity or is shared
|
|
||||||
// across modules.
|
|
||||||
static constexpr std::string_view kBreweryJsonGrammar = R"json_brewery(
|
static constexpr std::string_view kBreweryJsonGrammar = R"json_brewery(
|
||||||
root ::= thought-block "{" ws "\"name_en\"" ws ":" ws string ws "," ws "\"description_en\"" ws ":" ws string ws "," ws "\"name_local\"" ws ":" ws string ws "," ws "\"description_local\"" ws ":" ws string ws "}" ws
|
root ::= thought-block "{" ws "\"name_en\"" ws ":" ws string ws "," ws "\"description_en\"" ws ":" ws string ws "," ws "\"name_local\"" ws ":" ws string ws "," ws "\"description_local\"" ws ":" ws string ws "}" ws
|
||||||
thought-block ::= [^{]*
|
thought-block ::= [^{]*
|
||||||
@@ -61,12 +59,11 @@ BreweryResult LlamaGenerator::GenerateBrewery(
|
|||||||
location.country.empty() ? std::string{}
|
location.country.empty() ? std::string{}
|
||||||
: std::format(", {}", location.country);
|
: std::format(", {}", location.country);
|
||||||
/**
|
/**
|
||||||
* Load brewery system prompt via the injected prompt directory.
|
* Load brewery system prompt from file
|
||||||
* The key "BREWERY_GENERATION" resolves to BREWERY_GENERATION.md inside
|
* Falls back to minimal inline prompt if file not found
|
||||||
* the configured --prompt-dir. Throws on missing or empty file.
|
|
||||||
*/
|
*/
|
||||||
const std::string system_prompt =
|
const std::string system_prompt =
|
||||||
prompt_directory_->Load("BREWERY_GENERATION");
|
LoadBrewerySystemPrompt("prompts/system.md");
|
||||||
|
|
||||||
std::string user_prompt = std::format(
|
std::string user_prompt = std::format(
|
||||||
"## CITY:\n{}\n\n## COUNTRY:\n{}\n\n## LOCAL LANGUAGE CODES:\n{}\n\n## "
|
"## CITY:\n{}\n\n## COUNTRY:\n{}\n\n## LOCAL LANGUAGE CODES:\n{}\n\n## "
|
||||||
@@ -99,13 +96,8 @@ BreweryResult LlamaGenerator::GenerateBrewery(
|
|||||||
// Generate brewery data from LLM
|
// Generate brewery data from LLM
|
||||||
raw = this->Infer(system_prompt, user_prompt, max_tokens,
|
raw = this->Infer(system_prompt, user_prompt, max_tokens,
|
||||||
kBreweryJsonGrammar);
|
kBreweryJsonGrammar);
|
||||||
if (logger_) {
|
spdlog::debug("LlamaGenerator: raw output (attempt {}): {}", attempt + 1,
|
||||||
logger_->Log(
|
raw);
|
||||||
{.level = LogLevel::Debug,
|
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
|
||||||
.message = std::format("LlamaGenerator: raw output (attempt {}): {}",
|
|
||||||
attempt + 1, raw)});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate output: parse JSON and check required fields
|
// Validate output: parse JSON and check required fields
|
||||||
|
|
||||||
@@ -116,13 +108,9 @@ BreweryResult LlamaGenerator::GenerateBrewery(
|
|||||||
if (!validation_error.has_value()) {
|
if (!validation_error.has_value()) {
|
||||||
// Success: return parsed brewery data
|
// Success: return parsed brewery data
|
||||||
|
|
||||||
if (logger_) {
|
spdlog::info(
|
||||||
logger_->Log(
|
"LlamaGenerator: successfully generated brewery data on attempt {}",
|
||||||
{.level = LogLevel::Info,
|
attempt + 1);
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
|
||||||
.message = std::format("LlamaGenerator: successfully generated brewery data on attempt {}",
|
|
||||||
attempt + 1)});
|
|
||||||
}
|
|
||||||
|
|
||||||
return brewery;
|
return brewery;
|
||||||
}
|
}
|
||||||
@@ -130,14 +118,8 @@ BreweryResult LlamaGenerator::GenerateBrewery(
|
|||||||
// Validation failed: log error and prepare corrective feedback
|
// Validation failed: log error and prepare corrective feedback
|
||||||
|
|
||||||
last_error = *validation_error;
|
last_error = *validation_error;
|
||||||
if (logger_) {
|
spdlog::warn("LlamaGenerator: malformed brewery JSON (attempt {}): {}",
|
||||||
logger_->Log(
|
attempt + 1, *validation_error);
|
||||||
{.level = LogLevel::Warn,
|
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
|
||||||
.message =
|
|
||||||
std::format("LlamaGenerator: malformed brewery JSON (attempt {}): {}",
|
|
||||||
attempt + 1, *validation_error)});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update prompt with error details to guide LLM toward correct output.
|
// Update prompt with error details to guide LLM toward correct output.
|
||||||
user_prompt = std::format(
|
user_prompt = std::format(
|
||||||
@@ -154,13 +136,9 @@ BreweryResult LlamaGenerator::GenerateBrewery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All retry attempts exhausted: log failure and throw exception
|
// All retry attempts exhausted: log failure and throw exception
|
||||||
if (logger_) {
|
spdlog::error(
|
||||||
logger_->Log(
|
"LlamaGenerator: malformed brewery response after {} attempts: "
|
||||||
{.level = LogLevel::Error,
|
"{}",
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
max_attempts, last_error.empty() ? raw : last_error);
|
||||||
.message = std::format(
|
|
||||||
"LlamaGenerator: malformed brewery response after {} attempts: {}",
|
|
||||||
max_attempts, last_error.empty() ? raw : last_error)});
|
|
||||||
}
|
|
||||||
throw std::runtime_error("LlamaGenerator: malformed brewery response");
|
throw std::runtime_error("LlamaGenerator: malformed brewery response");
|
||||||
}
|
}
|
||||||
18
pipeline/src/data_generation/llama/generate_user.cc
Normal file
18
pipeline/src/data_generation/llama/generate_user.cc
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "data_generation/llama_generator.h"
|
||||||
|
#include "data_generation/llama_generator_helpers.h"
|
||||||
|
|
||||||
|
UserResult LlamaGenerator::GenerateUser(const std::string& locale) {
|
||||||
|
return {.username = "test_user",
|
||||||
|
.bio = "This is a test user profile from " + locale + "."};
|
||||||
|
}
|
||||||
@@ -16,11 +16,11 @@
|
|||||||
|
|
||||||
#include "data_generation/llama_generator_helpers.h"
|
#include "data_generation/llama_generator_helpers.h"
|
||||||
#include "llama.h"
|
#include "llama.h"
|
||||||
namespace {
|
|
||||||
/**
|
/**
|
||||||
* String trimming: removes leading and trailing whitespace
|
* String trimming: removes leading and trailing whitespace
|
||||||
*/
|
*/
|
||||||
std::string Trim(std::string_view value) {
|
static std::string Trim(std::string_view value) {
|
||||||
constexpr std::string_view whitespace = " \t\n\r\f\v";
|
constexpr std::string_view whitespace = " \t\n\r\f\v";
|
||||||
const size_t first_index = value.find_first_not_of(whitespace);
|
const size_t first_index = value.find_first_not_of(whitespace);
|
||||||
if (first_index == std::string_view::npos) {
|
if (first_index == std::string_view::npos) {
|
||||||
@@ -35,7 +35,7 @@ std::string Trim(std::string_view value) {
|
|||||||
* Normalize whitespace: collapses multiple spaces/tabs/newlines into single
|
* Normalize whitespace: collapses multiple spaces/tabs/newlines into single
|
||||||
* spaces
|
* spaces
|
||||||
*/
|
*/
|
||||||
std::string CondenseWhitespace(std::string_view text) {
|
static std::string CondenseWhitespace(std::string_view text) {
|
||||||
std::string out;
|
std::string out;
|
||||||
out.reserve(text.size());
|
out.reserve(text.size());
|
||||||
|
|
||||||
@@ -58,41 +58,6 @@ std::string CondenseWhitespace(std::string_view text) {
|
|||||||
return out;
|
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(const std::array<std::string*, 4>& values) {
|
|
||||||
for (const std::string* value : values) {
|
|
||||||
std::string lowered = *value;
|
|
||||||
std::ranges::transform(lowered, lowered.begin(),
|
|
||||||
[](const unsigned char character) {
|
|
||||||
return static_cast<char>(std::tolower(character));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lowered == "string") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate region context to fit within max length while preserving word
|
* Truncate region context to fit within max length while preserving word
|
||||||
* boundaries
|
* boundaries
|
||||||
@@ -106,8 +71,7 @@ std::string PrepareRegionContext(std::string_view region_context,
|
|||||||
|
|
||||||
normalized.resize(max_chars);
|
normalized.resize(max_chars);
|
||||||
const size_t last_space = normalized.find_last_of(' ');
|
const size_t last_space = normalized.find_last_of(' ');
|
||||||
if (last_space != std::string::npos &&
|
if (last_space != std::string::npos && last_space > max_chars / 2) {
|
||||||
last_space > max_chars / kTruncationGuardDivisor) {
|
|
||||||
normalized.resize(last_space);
|
normalized.resize(last_space);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +115,47 @@ void AppendTokenPiece(const llama_vocab* vocab, llama_token token,
|
|||||||
"LlamaGenerator: failed to decode sampled token piece");
|
"LlamaGenerator: failed to decode sampled token piece");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static 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 =
|
||||||
|
"JSON field '" + std::string(key) + "' is missing or not a string";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& string_value = field->as_string();
|
||||||
|
out = Trim(std::string_view(string_value.data(), string_value.size()));
|
||||||
|
if (out.empty()) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "JSON field '" + std::string(key) + "' must not be empty";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool HasSchemaPlaceholder(const std::array<std::string*, 4>& values) {
|
||||||
|
for (const std::string* value : values) {
|
||||||
|
std::string lowered = *value;
|
||||||
|
std::ranges::transform(lowered, lowered.begin(),
|
||||||
|
[](unsigned char character) {
|
||||||
|
return static_cast<char>(std::tolower(character));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lowered == "string") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<std::string> ValidateBreweryJson(const std::string& raw,
|
std::optional<std::string> ValidateBreweryJson(const std::string& raw,
|
||||||
BreweryResult& brewery_out) {
|
BreweryResult& brewery_out) {
|
||||||
boost::system::error_code error_code;
|
boost::system::error_code error_code;
|
||||||
@@ -198,7 +203,7 @@ std::optional<std::string> ValidateBreweryJson(const std::string& raw,
|
|||||||
return validation_error;
|
return validation_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::array schema_placeholders = {
|
const std::array<std::string*, 4> schema_placeholders = {
|
||||||
&brewery_out.name_en, &brewery_out.description_en,
|
&brewery_out.name_en, &brewery_out.description_en,
|
||||||
&brewery_out.name_local, &brewery_out.description_local};
|
&brewery_out.name_local, &brewery_out.description_local};
|
||||||
if (HasSchemaPlaceholder(schema_placeholders)) {
|
if (HasSchemaPlaceholder(schema_placeholders)) {
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
* output tokens back to text for system+user chat prompts.
|
* output tokens back to text for system+user chat prompts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
|
||||||
#include <format>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -19,9 +19,6 @@
|
|||||||
#include "llama.h"
|
#include "llama.h"
|
||||||
|
|
||||||
static constexpr size_t kPromptTokenSlack = 8;
|
static constexpr size_t kPromptTokenSlack = 8;
|
||||||
// Minimum tokens to keep when using top-p sampling. Ensures at least one
|
|
||||||
// candidate token remains available even with very restrictive top-p values.
|
|
||||||
static constexpr size_t kTopPMinKeep = 1;
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -65,7 +62,7 @@ SamplerHandle MakeSamplerChain(const llama_vocab* vocab,
|
|||||||
"LlamaGenerator: failed to initialize temperature sampler");
|
"LlamaGenerator: failed to initialize temperature sampler");
|
||||||
add_sampler(llama_sampler_init_top_k(static_cast<int32_t>(config.top_k)),
|
add_sampler(llama_sampler_init_top_k(static_cast<int32_t>(config.top_k)),
|
||||||
"LlamaGenerator: failed to initialize top-k sampler");
|
"LlamaGenerator: failed to initialize top-k sampler");
|
||||||
add_sampler(llama_sampler_init_top_p(config.top_p, kTopPMinKeep),
|
add_sampler(llama_sampler_init_top_p(config.top_p, 1),
|
||||||
"LlamaGenerator: failed to initialize top-p sampler");
|
"LlamaGenerator: failed to initialize top-p sampler");
|
||||||
add_sampler(llama_sampler_init_dist(config.seed),
|
add_sampler(llama_sampler_init_dist(config.seed),
|
||||||
"LlamaGenerator: failed to initialize distribution sampler");
|
"LlamaGenerator: failed to initialize distribution sampler");
|
||||||
@@ -107,7 +104,7 @@ std::string LlamaGenerator::InferFormatted(const std::string& formatted_prompt,
|
|||||||
.top_p = sampling_top_p_,
|
.top_p = sampling_top_p_,
|
||||||
.seed = static_cast<uint32_t>(rng_()),
|
.seed = static_cast<uint32_t>(rng_()),
|
||||||
};
|
};
|
||||||
const auto sampler = MakeSamplerChain(vocab, sampler_config, grammar);
|
auto sampler = MakeSamplerChain(vocab, sampler_config, grammar);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear KV cache to ensure clean inference state (no residual context)
|
* Clear KV cache to ensure clean inference state (no residual context)
|
||||||
@@ -171,14 +168,10 @@ std::string LlamaGenerator::InferFormatted(const std::string& formatted_prompt,
|
|||||||
*/
|
*/
|
||||||
prompt_tokens.resize(static_cast<size_t>(token_count));
|
prompt_tokens.resize(static_cast<size_t>(token_count));
|
||||||
if (token_count > prompt_budget) {
|
if (token_count > prompt_budget) {
|
||||||
if (logger_) {
|
spdlog::warn(
|
||||||
logger_->Log({.level = LogLevel::Warn,
|
"LlamaGenerator: prompt too long ({} tokens), truncating to {} "
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
"tokens to fit n_batch/n_ctx limits",
|
||||||
.message = std::format(
|
token_count, prompt_budget);
|
||||||
"LlamaGenerator: prompt too long ({} tokens), "
|
|
||||||
"truncating to {} tokens to fit n_batch/n_ctx limits",
|
|
||||||
token_count, prompt_budget)});
|
|
||||||
}
|
|
||||||
prompt_tokens.resize(static_cast<size_t>(prompt_budget));
|
prompt_tokens.resize(static_cast<size_t>(prompt_budget));
|
||||||
token_count = prompt_budget;
|
token_count = prompt_budget;
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "data_model/models.h"
|
#include "data_model/application_options.h"
|
||||||
#include "llama.h"
|
#include "llama.h"
|
||||||
|
|
||||||
static constexpr uint32_t kMaxContextSize = 32768U;
|
static constexpr uint32_t kMaxContextSize = 32768U;
|
||||||
@@ -32,13 +32,9 @@ void LlamaGenerator::ContextDeleter::operator()(
|
|||||||
|
|
||||||
LlamaGenerator::LlamaGenerator(
|
LlamaGenerator::LlamaGenerator(
|
||||||
const ApplicationOptions& options, const std::string& model_path,
|
const ApplicationOptions& options, const std::string& model_path,
|
||||||
std::shared_ptr<ILogger> logger,
|
std::unique_ptr<IPromptFormatter> prompt_formatter)
|
||||||
std::unique_ptr<IPromptFormatter> prompt_formatter,
|
|
||||||
std::unique_ptr<IPromptDirectory> prompt_directory)
|
|
||||||
: rng_(std::random_device{}()),
|
: rng_(std::random_device{}()),
|
||||||
logger_(std::move(logger)),
|
prompt_formatter_(std::move(prompt_formatter)) {
|
||||||
prompt_formatter_(std::move(prompt_formatter)),
|
|
||||||
prompt_directory_(std::move(prompt_directory)) {
|
|
||||||
if (model_path.empty()) {
|
if (model_path.empty()) {
|
||||||
throw std::runtime_error("LlamaGenerator: model path must not be empty");
|
throw std::runtime_error("LlamaGenerator: model path must not be empty");
|
||||||
}
|
}
|
||||||
@@ -48,50 +44,41 @@ LlamaGenerator::LlamaGenerator(
|
|||||||
"LlamaGenerator: prompt formatter dependency must not be null");
|
"LlamaGenerator: prompt formatter dependency must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prompt_directory_) {
|
if (options.temperature < 0.0F) {
|
||||||
throw std::runtime_error(
|
|
||||||
"LlamaGenerator: prompt directory dependency must not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto sampling = options.generator.sampling.value_or(SamplingOptions{});
|
|
||||||
|
|
||||||
if (sampling.temperature < 0.0F) {
|
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: sampling temperature must be >= 0");
|
"LlamaGenerator: sampling temperature must be >= 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampling.top_p <= 0.0F || sampling.top_p > 1.0F) {
|
if (options.top_p <= 0.0F || options.top_p > 1.0F) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: sampling top-p must be in (0, 1]");
|
"LlamaGenerator: sampling top-p must be in (0, 1]");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampling.top_k == 0U) {
|
if (options.top_k == 0U) {
|
||||||
throw std::runtime_error("LlamaGenerator: sampling top-k must be > 0");
|
throw std::runtime_error("LlamaGenerator: sampling top-k must be > 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampling.seed < -1) {
|
if (options.seed < -1) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: seed must be >= 0, or -1 for random");
|
"LlamaGenerator: seed must be >= 0, or -1 for random");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampling.n_ctx == 0 || sampling.n_ctx > kMaxContextSize) {
|
if (options.n_ctx == 0 || options.n_ctx > kMaxContextSize) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: context size must be in range [1, 32768]");
|
"LlamaGenerator: context size must be in range [1, 32768]");
|
||||||
}
|
}
|
||||||
|
|
||||||
sampling_temperature_ = sampling.temperature;
|
sampling_temperature_ = options.temperature;
|
||||||
sampling_top_p_ = sampling.top_p;
|
sampling_top_p_ = options.top_p;
|
||||||
sampling_top_k_ = sampling.top_k;
|
sampling_top_k_ = options.top_k;
|
||||||
|
|
||||||
if (sampling.seed == -1) {
|
if (options.seed == -1) {
|
||||||
std::random_device random_device;
|
std::random_device random_device;
|
||||||
rng_.seed(random_device());
|
rng_.seed(random_device());
|
||||||
} else {
|
} else {
|
||||||
rng_.seed(static_cast<uint32_t>(sampling.seed));
|
rng_.seed(static_cast<uint32_t>(options.seed));
|
||||||
}
|
}
|
||||||
|
n_ctx_ = options.n_ctx;
|
||||||
n_ctx_ = sampling.n_ctx;
|
|
||||||
n_gpu_layers_ = sampling.n_gpu_layers;
|
|
||||||
|
|
||||||
this->Load(model_path);
|
this->Load(model_path);
|
||||||
}
|
}
|
||||||
@@ -4,34 +4,23 @@
|
|||||||
* context, and resets prior resources during model initialization.
|
* context, and resets prior resources during model initialization.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "data_generation/llama_generator.h"
|
#include "data_generation/llama_generator.h"
|
||||||
#include "ggml-backend.h"
|
|
||||||
#include "llama.h"
|
#include "llama.h"
|
||||||
|
|
||||||
// Maximum batch size for decode operations. Capping the batch prevents
|
|
||||||
// excessive memory allocation while maintaining inference performance.
|
|
||||||
static constexpr uint32_t kMaxBatchSize = 5000U;
|
|
||||||
|
|
||||||
void LlamaGenerator::Load(const std::string& model_path) {
|
void LlamaGenerator::Load(const std::string& model_path) {
|
||||||
context_.reset();
|
context_.reset();
|
||||||
model_.reset();
|
model_.reset();
|
||||||
|
|
||||||
// Specifically load dynamic ggml backends (like CUDA) that are provided
|
const llama_model_params model_params = llama_model_default_params();
|
||||||
// externally before attempting to load a model.
|
LlamaGenerator::ModelHandle loaded_model(
|
||||||
ggml_backend_load_all();
|
|
||||||
|
|
||||||
llama_model_params model_params = llama_model_default_params();
|
|
||||||
model_params.n_gpu_layers = n_gpu_layers_;
|
|
||||||
|
|
||||||
ModelHandle loaded_model(
|
|
||||||
llama_model_load_from_file(model_path.c_str(), model_params));
|
llama_model_load_from_file(model_path.c_str(), model_params));
|
||||||
|
|
||||||
if (!loaded_model) {
|
if (!loaded_model) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: failed to load model from path: " + model_path);
|
"LlamaGenerator: failed to load model from path: " + model_path);
|
||||||
@@ -39,11 +28,10 @@ void LlamaGenerator::Load(const std::string& model_path) {
|
|||||||
|
|
||||||
llama_context_params context_params = llama_context_default_params();
|
llama_context_params context_params = llama_context_default_params();
|
||||||
context_params.n_ctx = n_ctx_;
|
context_params.n_ctx = n_ctx_;
|
||||||
context_params.n_batch = std::min(n_ctx_, kMaxBatchSize);
|
context_params.n_batch = std::min(n_ctx_, static_cast<uint32_t>(5000));
|
||||||
|
|
||||||
ContextHandle loaded_context(
|
LlamaGenerator::ContextHandle loaded_context(
|
||||||
llama_init_from_model(loaded_model.get(), context_params));
|
llama_init_from_model(loaded_model.get(), context_params));
|
||||||
|
|
||||||
if (!loaded_context) {
|
if (!loaded_context) {
|
||||||
throw std::runtime_error("LlamaGenerator: failed to create context");
|
throw std::runtime_error("LlamaGenerator: failed to create context");
|
||||||
}
|
}
|
||||||
@@ -51,10 +39,5 @@ void LlamaGenerator::Load(const std::string& model_path) {
|
|||||||
model_ = std::move(loaded_model);
|
model_ = std::move(loaded_model);
|
||||||
context_ = std::move(loaded_context);
|
context_ = std::move(loaded_context);
|
||||||
|
|
||||||
if (logger_) {
|
spdlog::info("[LlamaGenerator] Loaded model: {}", model_path);
|
||||||
logger_->Log({.level = LogLevel::Info,
|
|
||||||
.phase = PipelinePhase::Startup,
|
|
||||||
.message = std::format("[LlamaGenerator] Loaded model: {} ",
|
|
||||||
model_path)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
55
pipeline/src/data_generation/llama/load_brewery_prompt.cc
Normal file
55
pipeline/src/data_generation/llama/load_brewery_prompt.cc
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* @file data_generation/llama/load_brewery_prompt.cc
|
||||||
|
* @brief Resolves brewery system prompt content from cache or a configured
|
||||||
|
* filesystem path and provides a robust inline fallback prompt when absent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "data_generation/llama_generator.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Loads brewery system prompt from disk or cache.
|
||||||
|
*
|
||||||
|
* @param prompt_file_path Preferred prompt file location.
|
||||||
|
* @return Prompt text loaded from disk.
|
||||||
|
*/
|
||||||
|
std::string LlamaGenerator::LoadBrewerySystemPrompt(
|
||||||
|
const std::filesystem::path& prompt_file_path) {
|
||||||
|
// Return cached version if already loaded
|
||||||
|
if (!brewery_system_prompt_.empty()) {
|
||||||
|
return brewery_system_prompt_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream prompt_file(prompt_file_path);
|
||||||
|
if (!prompt_file.is_open()) {
|
||||||
|
spdlog::error(
|
||||||
|
"LlamaGenerator: Failed to open brewery system prompt file '{}'",
|
||||||
|
prompt_file_path.string());
|
||||||
|
throw std::runtime_error(
|
||||||
|
"LlamaGenerator: missing brewery system prompt file: " +
|
||||||
|
prompt_file_path.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string prompt((std::istreambuf_iterator(prompt_file)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
prompt_file.close();
|
||||||
|
|
||||||
|
if (prompt.empty()) {
|
||||||
|
spdlog::error("LlamaGenerator: Brewery system prompt file '{}' is empty",
|
||||||
|
prompt_file_path.string());
|
||||||
|
throw std::runtime_error(
|
||||||
|
"LlamaGenerator: empty brewery system prompt file: " +
|
||||||
|
prompt_file_path.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
spdlog::info(
|
||||||
|
"LlamaGenerator: Loaded brewery system prompt from '{}' ({} chars)",
|
||||||
|
prompt_file_path.string(), prompt.length());
|
||||||
|
brewery_system_prompt_ = prompt;
|
||||||
|
return brewery_system_prompt_;
|
||||||
|
}
|
||||||
@@ -17,9 +17,9 @@ BreweryResult MockGenerator::GenerateBrewery(
|
|||||||
const std::string_view adjective =
|
const std::string_view adjective =
|
||||||
kBreweryAdjectives.at(hash % kBreweryAdjectives.size());
|
kBreweryAdjectives.at(hash % kBreweryAdjectives.size());
|
||||||
const std::string_view noun =
|
const std::string_view noun =
|
||||||
kBreweryNouns.at(hash / kNounHashStride % kBreweryNouns.size());
|
kBreweryNouns.at(hash / 7 % kBreweryNouns.size());
|
||||||
const std::string_view base_description = kBreweryDescriptions.at(
|
const std::string_view base_description =
|
||||||
(hash / kDescriptionHashStride) % kBreweryDescriptions.size());
|
kBreweryDescriptions.at((hash / 13) % kBreweryDescriptions.size());
|
||||||
|
|
||||||
const std::string name =
|
const std::string name =
|
||||||
std::format("{} {} {}", location.city, adjective, noun);
|
std::format("{} {} {}", location.city, adjective, noun);
|
||||||
@@ -15,7 +15,7 @@ UserResult MockGenerator::GenerateUser(const std::string& locale) {
|
|||||||
|
|
||||||
UserResult result;
|
UserResult result;
|
||||||
const std::string_view username = kUsernames[hash % kUsernames.size()];
|
const std::string_view username = kUsernames[hash % kUsernames.size()];
|
||||||
const std::string_view bio = kBios[hash / kBioHashStride % kBios.size()];
|
const std::string_view bio = kBios[hash / 11 % kBios.size()];
|
||||||
result.username = username;
|
result.username = username;
|
||||||
result.bio = bio;
|
result.bio = bio;
|
||||||
return result;
|
return result;
|
||||||
@@ -6,9 +6,7 @@
|
|||||||
|
|
||||||
#include "json_handling/json_loader.h"
|
#include "json_handling/json_loader.h"
|
||||||
|
|
||||||
#include <format>
|
#include <spdlog/spdlog.h>
|
||||||
#include "services/logging/logger.h"
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include <boost/json.hpp>
|
#include <boost/json.hpp>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -21,8 +19,8 @@ static std::string ReadRequiredString(const boost::json::object& object,
|
|||||||
const char* key) {
|
const char* key) {
|
||||||
const boost::json::value* value = object.if_contains(key);
|
const boost::json::value* value = object.if_contains(key);
|
||||||
if (value == nullptr || !value->is_string()) {
|
if (value == nullptr || !value->is_string()) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(std::string("Missing or invalid string field: ") +
|
||||||
std::format("Missing or invalid string field: {}", key));
|
key);
|
||||||
}
|
}
|
||||||
const std::string_view text = value->as_string();
|
const std::string_view text = value->as_string();
|
||||||
return std::string(text);
|
return std::string(text);
|
||||||
@@ -32,8 +30,8 @@ static double ReadRequiredNumber(const boost::json::object& object,
|
|||||||
const char* key) {
|
const char* key) {
|
||||||
const boost::json::value* value = object.if_contains(key);
|
const boost::json::value* value = object.if_contains(key);
|
||||||
if (value == nullptr || !value->is_number()) {
|
if (value == nullptr || !value->is_number()) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(std::string("Missing or invalid numeric field: ") +
|
||||||
std::format("Missing or invalid numeric field: {}", key));
|
key);
|
||||||
}
|
}
|
||||||
return value->to_number<double>();
|
return value->to_number<double>();
|
||||||
}
|
}
|
||||||
@@ -43,7 +41,7 @@ static std::vector<std::string> ReadRequiredStringArray(
|
|||||||
const boost::json::value* value = object.if_contains(key);
|
const boost::json::value* value = object.if_contains(key);
|
||||||
if (value == nullptr || !value->is_array()) {
|
if (value == nullptr || !value->is_array()) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
std::format("Missing or invalid string array field: {}", key));
|
std::string("Missing or invalid string array field: ") + key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& array = value->as_array();
|
const auto& array = value->as_array();
|
||||||
@@ -52,7 +50,7 @@ static std::vector<std::string> ReadRequiredStringArray(
|
|||||||
for (const auto& item : array) {
|
for (const auto& item : array) {
|
||||||
if (!item.is_string()) {
|
if (!item.is_string()) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
std::format("Missing or invalid string array field: {}", key));
|
std::string("Missing or invalid string array field: ") + key);
|
||||||
}
|
}
|
||||||
items.emplace_back(item.as_string());
|
items.emplace_back(item.as_string());
|
||||||
}
|
}
|
||||||
@@ -60,7 +58,7 @@ static std::vector<std::string> ReadRequiredStringArray(
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Location> JsonLoader::LoadLocations(
|
std::vector<Location> JsonLoader::LoadLocations(
|
||||||
const std::filesystem::path& filepath, std::shared_ptr<ILogger> logger) {
|
const std::filesystem::path& filepath) {
|
||||||
std::ifstream input(filepath);
|
std::ifstream input(filepath);
|
||||||
if (!input.is_open()) {
|
if (!input.is_open()) {
|
||||||
throw std::runtime_error("Failed to open locations file: " +
|
throw std::runtime_error("Failed to open locations file: " +
|
||||||
@@ -106,5 +104,7 @@ std::vector<Location> JsonLoader::LoadLocations(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spdlog::info("[JsonLoader] Loaded {} locations from {}", locations.size(),
|
||||||
|
filepath.string());
|
||||||
return locations;
|
return locations;
|
||||||
}
|
}
|
||||||
198
pipeline/src/main.cc
Normal file
198
pipeline/src/main.cc
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* @file main.cc
|
||||||
|
* @brief Parses command-line options, validates runtime mode selection,
|
||||||
|
* initializes shared infrastructure, and executes the pipeline entry flow.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <boost/di.hpp>
|
||||||
|
#include <boost/program_options.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <exception>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "biergarten_data_generator.h"
|
||||||
|
#include "data_generation/llama_generator.h"
|
||||||
|
#include "data_generation/mock_generator.h"
|
||||||
|
#include "data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.h"
|
||||||
|
#include "data_model/application_options.h"
|
||||||
|
#include "llama_backend_state.h"
|
||||||
|
#include "services/enrichment_service.h"
|
||||||
|
#include "services/export_service.h"
|
||||||
|
#include "services/sqlite_export_service.h"
|
||||||
|
#include "services/wikipedia_service.h"
|
||||||
|
#include "web_client/curl_web_client.h"
|
||||||
|
|
||||||
|
namespace prog_opts = boost::program_options;
|
||||||
|
namespace di = boost::di;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse command-line arguments into ApplicationOptions.
|
||||||
|
*
|
||||||
|
* @param argc Command-line argument count.
|
||||||
|
* @param argv Command-line arguments.
|
||||||
|
* @return Parsed ApplicationOptions if parsing succeeded, std::nullopt
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
std::optional<ApplicationOptions> ParseArguments(const int argc, char** argv) {
|
||||||
|
prog_opts::options_description desc("Pipeline Options");
|
||||||
|
|
||||||
|
auto opt = desc.add_options();
|
||||||
|
|
||||||
|
opt("help,h", "Produce help message");
|
||||||
|
|
||||||
|
opt("mocked", prog_opts::bool_switch(),
|
||||||
|
"Use mocked generator for brewery/user data");
|
||||||
|
|
||||||
|
opt("model,m", prog_opts::value<std::string>()->default_value(""),
|
||||||
|
"Path to LLM model (gguf)");
|
||||||
|
|
||||||
|
opt("temperature", prog_opts::value<float>()->default_value(1.0F),
|
||||||
|
"Sampling temperature (higher = more random)");
|
||||||
|
|
||||||
|
opt("top-p", prog_opts::value<float>()->default_value(0.95F),
|
||||||
|
"Nucleus sampling top-p in (0,1] (higher = more random)");
|
||||||
|
|
||||||
|
opt("top-k", prog_opts::value<uint32_t>()->default_value(64),
|
||||||
|
"Top-k sampling parameter (higher = more candidate tokens)");
|
||||||
|
|
||||||
|
opt("n-ctx", prog_opts::value<uint32_t>()->default_value(8192),
|
||||||
|
"Context window size in tokens (1-32768)");
|
||||||
|
|
||||||
|
opt("seed", prog_opts::value<int>()->default_value(-1),
|
||||||
|
"Sampler seed: -1 for random, otherwise non-negative integer");
|
||||||
|
|
||||||
|
// Handle the "no arguments" or "help" case
|
||||||
|
if (argc == 1) {
|
||||||
|
spdlog::info("Biergarten Pipeline");
|
||||||
|
std::stringstream usage_stream;
|
||||||
|
usage_stream << "\nUsage: biergarten-pipeline [options]\n\n" << desc;
|
||||||
|
spdlog::info(usage_stream.str());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
prog_opts::variables_map variables_map;
|
||||||
|
prog_opts::store(prog_opts::parse_command_line(argc, argv, desc),
|
||||||
|
variables_map);
|
||||||
|
prog_opts::notify(variables_map);
|
||||||
|
|
||||||
|
if (variables_map.contains("help")) {
|
||||||
|
std::stringstream help_stream;
|
||||||
|
help_stream << "\n" << desc;
|
||||||
|
spdlog::info(help_stream.str());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto use_mocked = variables_map["mocked"].as<bool>();
|
||||||
|
const auto model_path = variables_map["model"].as<std::string>();
|
||||||
|
|
||||||
|
if (use_mocked && !model_path.empty()) {
|
||||||
|
spdlog::error(
|
||||||
|
"Invalid arguments: --mocked and --model are mutually exclusive");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!use_mocked && model_path.empty()) {
|
||||||
|
spdlog::error(
|
||||||
|
"Invalid arguments: Either --mocked or --model must be specified");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool has_llm_params = !variables_map["temperature"].defaulted() ||
|
||||||
|
!variables_map["top-p"].defaulted() ||
|
||||||
|
!variables_map["top-k"].defaulted() ||
|
||||||
|
!variables_map["seed"].defaulted();
|
||||||
|
|
||||||
|
if (use_mocked && has_llm_params) {
|
||||||
|
spdlog::warn(
|
||||||
|
"Sampling parameters (--temperature, --top-p, --top-k, --seed) are"
|
||||||
|
" ignored when using --mocked");
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationOptions options;
|
||||||
|
options.use_mocked = use_mocked;
|
||||||
|
options.model_path = model_path;
|
||||||
|
options.temperature = variables_map["temperature"].as<float>();
|
||||||
|
options.top_p = variables_map["top-p"].as<float>();
|
||||||
|
options.top_k = variables_map["top-k"].as<uint32_t>();
|
||||||
|
options.n_ctx = variables_map["n-ctx"].as<uint32_t>();
|
||||||
|
options.seed = variables_map["seed"].as<int>();
|
||||||
|
|
||||||
|
return options;
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Failed to parse command-line arguments: {}",
|
||||||
|
exception.what());
|
||||||
|
return std::nullopt;
|
||||||
|
} catch (...) {
|
||||||
|
spdlog::error("Failed to parse command-line arguments: unknown error");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Timer {
|
||||||
|
std::chrono::steady_clock::time_point start_time =
|
||||||
|
std::chrono::steady_clock::now();
|
||||||
|
[[nodiscard]] int64_t Elapsed() const {
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now() - start_time)
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(const int argc, char** argv) {
|
||||||
|
try {
|
||||||
|
Timer timer;
|
||||||
|
const CurlGlobalState curl_state;
|
||||||
|
const LlamaBackendState llama_backend_state;
|
||||||
|
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");
|
||||||
|
|
||||||
|
const auto parsed_options = ParseArguments(argc, argv);
|
||||||
|
if (!parsed_options.has_value()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto options = *parsed_options;
|
||||||
|
|
||||||
|
const auto injector = di::make_injector(
|
||||||
|
di::bind<WebClient>().to<CURLWebClient>(),
|
||||||
|
di::bind<ApplicationOptions>().to(options),
|
||||||
|
di::bind<IEnrichmentService>().to<WikipediaService>(),
|
||||||
|
di::bind<IExportService>().to<SqliteExportService>(),
|
||||||
|
di::bind<IPromptFormatter>().to<Gemma4JinjaPromptFormatter>(),
|
||||||
|
di::bind<std::string>().to(options.model_path),
|
||||||
|
di::bind<DataGenerator>().to(
|
||||||
|
[options](const auto& inj) -> std::unique_ptr<DataGenerator> {
|
||||||
|
if (options.use_mocked) {
|
||||||
|
spdlog::info(
|
||||||
|
"[Generator] Using MockGenerator (no model path provided)");
|
||||||
|
return std::make_unique<MockGenerator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
spdlog::info(
|
||||||
|
"[Generator] Using LlamaGenerator: {} (temperature={}, "
|
||||||
|
"top-p={}, top-k={}, n_ctx={}, seed={})",
|
||||||
|
options.model_path, options.temperature, options.top_p,
|
||||||
|
options.top_k, options.n_ctx, options.seed);
|
||||||
|
return inj.template create<std::unique_ptr<LlamaGenerator>>();
|
||||||
|
}));
|
||||||
|
|
||||||
|
auto generator =
|
||||||
|
injector.create<std::unique_ptr<BiergartenDataGenerator>>();
|
||||||
|
|
||||||
|
if (!generator->Run()) {
|
||||||
|
spdlog::error("Pipeline execution failed");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
spdlog::info("Pipeline executed successfully in {} ms", timer.Elapsed());
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::critical("Unhandled fatal error in main: {}", exception.what());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
pipeline/src/services/sqlite/build_database_path.cc
Normal file
24
pipeline/src/services/sqlite/build_database_path.cc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* @file services/sqlite/build_database_path.cc
|
||||||
|
* @brief SqliteExportService::BuildDatabasePath() implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "services/sqlite_export_service.h"
|
||||||
|
|
||||||
|
std::filesystem::path SqliteExportService::BuildDatabasePath() const {
|
||||||
|
std::filesystem::path base_filename("biergarten_seed_" + run_timestamp_utc_ +
|
||||||
|
".sqlite");
|
||||||
|
std::filesystem::path candidate =
|
||||||
|
std::filesystem::current_path() / base_filename;
|
||||||
|
|
||||||
|
for (int suffix = 1; std::filesystem::exists(candidate); ++suffix) {
|
||||||
|
candidate = std::filesystem::current_path() /
|
||||||
|
std::filesystem::path("biergarten_seed_" + run_timestamp_utc_ +
|
||||||
|
"-" + std::to_string(suffix) + ".sqlite");
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
28
pipeline/src/services/sqlite/build_location_key.cc
Normal file
28
pipeline/src/services/sqlite/build_location_key.cc
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* @file services/sqlite/build_location_key.cc
|
||||||
|
* @brief SqliteExportService::BuildLocationKey() implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "services/sqlite_export_service.h"
|
||||||
|
#include "services/sqlite_export_service_helpers.h"
|
||||||
|
|
||||||
|
constexpr int kLocationPrecision = 17;
|
||||||
|
|
||||||
|
std::string SqliteExportService::BuildLocationKey(const Location& location) {
|
||||||
|
std::ostringstream key_stream;
|
||||||
|
key_stream << location.city << '\n'
|
||||||
|
<< location.state_province << '\n'
|
||||||
|
<< location.iso3166_2 << '\n'
|
||||||
|
<< location.country << '\n'
|
||||||
|
<< location.iso3166_1 << '\n'
|
||||||
|
<< std::setprecision(kLocationPrecision) << location.latitude
|
||||||
|
<< '\n'
|
||||||
|
<< std::setprecision(kLocationPrecision) << location.longitude
|
||||||
|
<< '\n'
|
||||||
|
<< sqlite_export_service_internal::SerializeLocalLanguages(
|
||||||
|
location.local_languages);
|
||||||
|
return key_stream.str();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user