feat: initial commit
This commit is contained in:
11
go.mod
11
go.mod
@ -6,8 +6,8 @@ toolchain go1.24.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.3.833
|
github.com/a-h/templ v0.3.833
|
||||||
|
github.com/bornholm/genai v0.0.0-20250522134458-696e8771bb84
|
||||||
github.com/caarlos0/env/v11 v11.3.1
|
github.com/caarlos0/env/v11 v11.3.1
|
||||||
github.com/davecgh/go-spew v1.1.1
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7
|
github.com/gabriel-vasile/mimetype v1.4.7
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/gorilla/sessions v1.1.1
|
github.com/gorilla/sessions v1.1.1
|
||||||
@ -23,11 +23,11 @@ require (
|
|||||||
cloud.google.com/go/compute v1.20.1 // indirect
|
cloud.google.com/go/compute v1.20.1 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/RealAlexandreAI/json-repair v0.0.14 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/mux v1.6.2 // indirect
|
github.com/gorilla/mux v1.6.2 // indirect
|
||||||
@ -35,8 +35,13 @@ require (
|
|||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/openai/openai-go v0.1.0-beta.10 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
github.com/revrost/go-openrouter v0.0.0-20250414052218-c9123df8a97e // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||||
golang.org/x/net v0.39.0 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
|
18
go.sum
18
go.sum
@ -4,8 +4,12 @@ cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGB
|
|||||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/RealAlexandreAI/json-repair v0.0.14 h1:4kTqotVonDVTio5n2yweRUELVcNe2x518wl0bCsw0t0=
|
||||||
|
github.com/RealAlexandreAI/json-repair v0.0.14/go.mod h1:GKJi5borR78O8c7HCVbgqjhoiVibZ6hJldxbc6dGrAI=
|
||||||
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
|
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
|
||||||
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
|
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
|
||||||
|
github.com/bornholm/genai v0.0.0-20250522134458-696e8771bb84 h1:p8v5GSFZrOkpTMBnlvVp7YQaNnoKW/9C43rpD8Od8G0=
|
||||||
|
github.com/bornholm/genai v0.0.0-20250522134458-696e8771bb84/go.mod h1:W9kbOIXt50p7EL8qmjFy//gk6EzVHigsH5NnkfdTvZ8=
|
||||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@ -64,6 +68,8 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp
|
|||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||||
|
github.com/openai/openai-go v0.1.0-beta.10 h1:CknhGXe8aXQMRuqg255PFnWzgRY9nEryMxoNIBBM9tU=
|
||||||
|
github.com/openai/openai-go v0.1.0-beta.10/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@ -71,10 +77,22 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/revrost/go-openrouter v0.0.0-20250414052218-c9123df8a97e h1:sh6V3xdRzQDyqHI+3tolFjQKj0wlqybS6q8cHnKqLaA=
|
||||||
|
github.com/revrost/go-openrouter v0.0.0-20250414052218-c9123df8a97e/go.mod h1:HRfNDVNl2YQCfH9k4d2LGRZQWs9Da5K6ByWfDqwQAkY=
|
||||||
github.com/samber/slog-http v1.4.4 h1:NuENLy39Lk6b7wfj9cG9R5C/JLZR4t6pb9cwlyroybI=
|
github.com/samber/slog-http v1.4.4 h1:NuENLy39Lk6b7wfj9cG9R5C/JLZR4t6pb9cwlyroybI=
|
||||||
github.com/samber/slog-http v1.4.4/go.mod h1:PAcQQrYFo5KM7Qbk50gNNwKEAMGCyfsw6GN5dI0iv9g=
|
github.com/samber/slog-http v1.4.4/go.mod h1:PAcQQrYFo5KM7Qbk50gNNwKEAMGCyfsw6GN5dI0iv9g=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
@ -11,6 +11,7 @@ type Config struct {
|
|||||||
HTTP HTTP `envPrefix:"HTTP_"`
|
HTTP HTTP `envPrefix:"HTTP_"`
|
||||||
Store Store `envPrefix:"STORE_"`
|
Store Store `envPrefix:"STORE_"`
|
||||||
Quiz Quiz `envPrefix:"QUIZ_"`
|
Quiz Quiz `envPrefix:"QUIZ_"`
|
||||||
|
LLM LLM `envPrefix:"LLM_"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse() (*Config, error) {
|
func Parse() (*Config, error) {
|
||||||
|
10
internal/config/llm.go
Normal file
10
internal/config/llm.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "github.com/bornholm/genai/llm/provider"
|
||||||
|
|
||||||
|
type LLM struct {
|
||||||
|
Provider provider.Name `env:"PROVIDER" envDefault:"mistral"`
|
||||||
|
BaseURL string `env:"BASE_URL" envDefault:"https://api.mistral.ai/v1/"`
|
||||||
|
APIKey string `env:"API_KEY"`
|
||||||
|
Model string `env:"MODEL" envDefault:"mistral-small-latest"`
|
||||||
|
}
|
41
internal/http/handler/api/handler.go
Normal file
41
internal/http/handler/api/handler.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/kouiz/internal/store"
|
||||||
|
"forge.cadoles.com/wpetit/kouiz/internal/timex"
|
||||||
|
"github.com/bornholm/genai/llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
mux *http.ServeMux
|
||||||
|
store *store.Store
|
||||||
|
llm llm.Client
|
||||||
|
|
||||||
|
playInterval time.Duration
|
||||||
|
playPeriod timex.PeriodType
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements http.Handler.
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.mux.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(store *store.Store, llm llm.Client, playInterval time.Duration, playPeriod timex.PeriodType) *Handler {
|
||||||
|
h := &Handler{
|
||||||
|
mux: http.NewServeMux(),
|
||||||
|
store: store,
|
||||||
|
llm: llm,
|
||||||
|
playInterval: playInterval,
|
||||||
|
playPeriod: playPeriod,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mux.HandleFunc("GET /presentation/leaderboard", h.getLeaderboardPresentation)
|
||||||
|
h.mux.HandleFunc("GET /presentation/turn", h.getTurnPresentation)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ http.Handler = &Handler{}
|
161
internal/http/handler/api/presentation.go
Normal file
161
internal/http/handler/api/presentation.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/kouiz/internal/store"
|
||||||
|
"github.com/bornholm/genai/llm"
|
||||||
|
"github.com/bornholm/genai/llm/prompt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const systemPromptTemplate string = `
|
||||||
|
Tu es "Panda", un présentateur de jeu télévisé charismatique et plein d'énergie qui anime un jeu de question/réponse en ligne. Ta personnalité est :
|
||||||
|
|
||||||
|
- Enthousiaste et dynamique : Tu t'exprimes avec beaucoup d'énergie, utilises des exclamations et des expressions colorées
|
||||||
|
- Bienveillant mais taquin : Tu encourages tous les joueurs tout en les chambrant gentiment
|
||||||
|
- Théâtral : Tu dramatises les situations, crées du suspense même pour les scores
|
||||||
|
- Métaphoriquement créatif : Tu utilises des comparaisons amusantes liées au monde du panda, de la nature ou de la culture pop
|
||||||
|
|
||||||
|
Ton style :
|
||||||
|
|
||||||
|
- Utilise des expressions comme "Extraordinaire !", "Incroyable retournement de situation !"
|
||||||
|
- Varie tes présentations : parfois comme un commentateur sportif, parfois comme un chroniqueur dramatique
|
||||||
|
- Ajoute des petites blagues ou références pop culture
|
||||||
|
- Maintiens le suspense même pour des écarts de score importants
|
||||||
|
- Format attendu : Présente les situations de manière narrative et engageante.
|
||||||
|
`
|
||||||
|
|
||||||
|
const leaderboardPromptTemplate string = `
|
||||||
|
Fais le bilan des scores du jeu de manière captivante et divertissante en 1 court paragraphe.
|
||||||
|
|
||||||
|
## Liste des joueurs
|
||||||
|
|
||||||
|
{{ range $rank, $player := .Players }}
|
||||||
|
### {{ $rank }}. {{ $player.Name }}
|
||||||
|
|
||||||
|
- **Score:** {{ $player.Score }}
|
||||||
|
{{ end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
func (h *Handler) getLeaderboardPresentation(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
systemPrompt, err := prompt.Template[any](systemPromptTemplate, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var players []*store.Player
|
||||||
|
err = h.store.Do(ctx, func(db *gorm.DB) error {
|
||||||
|
err := db.Model(&store.Player{}).
|
||||||
|
Select("id", "name", "score").
|
||||||
|
Order("score DESC").Find(&players).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userPrompt, err := prompt.Template(leaderboardPromptTemplate, struct {
|
||||||
|
Players []*store.Player
|
||||||
|
}{
|
||||||
|
Players: players,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.llm.ChatCompletion(ctx,
|
||||||
|
llm.WithMessages(
|
||||||
|
llm.NewMessage(llm.RoleSystem, systemPrompt),
|
||||||
|
llm.NewMessage(llm.RoleUser, userPrompt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.WriteString(w, res.Message().Content()); err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnPromptTemplate string = `
|
||||||
|
Présente le tour de jeu actuel en créant du suspense et en motivant les joueurs à participer en 1 court paragraphe.
|
||||||
|
|
||||||
|
## Tour #{{ .Turn.ID }}
|
||||||
|
|
||||||
|
### Thématiques disponibles pour ce tour
|
||||||
|
|
||||||
|
{{ range .Turn.Entries }}
|
||||||
|
#### {{ .Category.Theme }}
|
||||||
|
|
||||||
|
- **Difficulté:** {{ .Level }}
|
||||||
|
- **Catégorie:** {{ .Category.Name }}
|
||||||
|
- **Description de la catégorie:** {{ .Category.Description }}
|
||||||
|
{{ end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
func (h *Handler) getTurnPresentation(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
systemPrompt, err := prompt.Template[any](systemPromptTemplate, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTurn, err := h.store.GetQuizTurn(ctx, h.playInterval, h.playPeriod)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userPrompt, err := prompt.Template(turnPromptTemplate, struct {
|
||||||
|
Turn *store.QuizTurn
|
||||||
|
}{
|
||||||
|
Turn: currentTurn,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.llm.ChatCompletion(ctx,
|
||||||
|
llm.WithMessages(
|
||||||
|
llm.NewMessage(llm.RoleSystem, systemPrompt),
|
||||||
|
llm.NewMessage(llm.RoleUser, userPrompt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.WriteString(w, res.Message().Content()); err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
BIN
internal/http/handler/webui/common/assets/panda.png
Normal file
BIN
internal/http/handler/webui/common/assets/panda.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 232 KiB |
31
internal/setup/api_handler.go
Normal file
31
internal/setup/api_handler.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/kouiz/internal/config"
|
||||||
|
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/api"
|
||||||
|
"github.com/bornholm/genai/llm/provider"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
_ "github.com/bornholm/genai/llm/provider/all"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAPIHandlerFromConfig(ctx context.Context, conf *config.Config) (*api.Handler, error) {
|
||||||
|
store, err := getStoreFromConfig(ctx, conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
llm, err := provider.Create(ctx, provider.WithChatCompletionOptions(provider.ClientOptions{
|
||||||
|
Provider: conf.LLM.Provider,
|
||||||
|
BaseURL: conf.LLM.BaseURL,
|
||||||
|
APIKey: conf.LLM.APIKey,
|
||||||
|
Model: conf.LLM.Model,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.NewHandler(store, llm, conf.Quiz.PlayInterval, conf.Quiz.PlayPeriod), nil
|
||||||
|
}
|
34
internal/setup/helper.go
Normal file
34
internal/setup/helper.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/kouiz/internal/config"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createFromConfigOnce[T any](factory func(ctx context.Context, conf *config.Config) (T, error)) func(ctx context.Context, conf *config.Config) (T, error) {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
service T
|
||||||
|
onceErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
return func(ctx context.Context, conf *config.Config) (T, error) {
|
||||||
|
once.Do(func() {
|
||||||
|
srv, err := factory(ctx, conf)
|
||||||
|
if err != nil {
|
||||||
|
onceErr = errors.WithStack(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service = srv
|
||||||
|
})
|
||||||
|
if onceErr != nil {
|
||||||
|
return *new(T), onceErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
}
|
@ -2,25 +2,37 @@ package setup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"forge.cadoles.com/wpetit/kouiz/internal/config"
|
"forge.cadoles.com/wpetit/kouiz/internal/config"
|
||||||
"forge.cadoles.com/wpetit/kouiz/internal/http"
|
kouizHTTP "forge.cadoles.com/wpetit/kouiz/internal/http"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewHTTPServerFromConfig(ctx context.Context, conf *config.Config) (*http.Server, error) {
|
func NewHTTPServerFromConfig(ctx context.Context, conf *config.Config) (*kouizHTTP.Server, error) {
|
||||||
// Configure Web UI handler
|
// Configure Web UI handler
|
||||||
webui, err := NewWebUIHandlerFromConfig(ctx, conf)
|
webui, err := NewWebUIHandlerFromConfig(ctx, conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "could not configure webui handler from config")
|
return nil, errors.Wrap(err, "could not configure webui handler from config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure API handler
|
||||||
|
api, err := NewAPIHandlerFromConfig(ctx, conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not configure webui handler from config")
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.Handle("/", webui)
|
||||||
|
mux.Handle("/api/", http.StripPrefix("/api", api))
|
||||||
|
|
||||||
// Create HTTP server
|
// Create HTTP server
|
||||||
|
|
||||||
server := http.NewServer(
|
server := kouizHTTP.NewServer(
|
||||||
webui,
|
mux,
|
||||||
http.WithAddress(conf.HTTP.Address),
|
kouizHTTP.WithAddress(conf.HTTP.Address),
|
||||||
http.WithBaseURL(conf.HTTP.BaseURL),
|
kouizHTTP.WithBaseURL(conf.HTTP.BaseURL),
|
||||||
)
|
)
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
|
@ -3,34 +3,21 @@ package setup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"forge.cadoles.com/wpetit/kouiz/internal/config"
|
"forge.cadoles.com/wpetit/kouiz/internal/config"
|
||||||
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/quiz"
|
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/quiz"
|
||||||
"forge.cadoles.com/wpetit/kouiz/internal/store"
|
"forge.cadoles.com/wpetit/kouiz/internal/store"
|
||||||
"forge.cadoles.com/wpetit/kouiz/misc/quiz/openquizzdb"
|
"forge.cadoles.com/wpetit/kouiz/misc/quiz/openquizzdb"
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewQuizHandlerFromConfig(ctx context.Context, conf *config.Config) (*quiz.Handler, error) {
|
func NewQuizHandlerFromConfig(ctx context.Context, conf *config.Config) (*quiz.Handler, error) {
|
||||||
db, err := gorm.Open(sqlite.Open(string(conf.Store.DSN)), &gorm.Config{
|
store, err := getStoreFromConfig(ctx, conf)
|
||||||
NowFunc: func() time.Time {
|
|
||||||
return time.Now().UTC()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Store.Debug {
|
|
||||||
db = db.Debug()
|
|
||||||
}
|
|
||||||
|
|
||||||
store := store.New(db)
|
|
||||||
|
|
||||||
if err := loadEmbeddedQuizz(ctx, store); err != nil {
|
if err := loadEmbeddedQuizz(ctx, store); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
32
internal/setup/store.go
Normal file
32
internal/setup/store.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/kouiz/internal/config"
|
||||||
|
"forge.cadoles.com/wpetit/kouiz/internal/store"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getStoreFromConfig = createFromConfigOnce(func(ctx context.Context, conf *config.Config) (*store.Store, error) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(string(conf.Store.DSN)), &gorm.Config{
|
||||||
|
NowFunc: func() time.Time {
|
||||||
|
return time.Now().UTC()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Store.Debug {
|
||||||
|
db = db.Debug()
|
||||||
|
}
|
||||||
|
|
||||||
|
store := store.New(db)
|
||||||
|
|
||||||
|
return store, nil
|
||||||
|
|
||||||
|
})
|
Reference in New Issue
Block a user