25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

194 satır
6.8KB

  1. package app
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "net/http"
  8. "strings"
  9. "time"
  10. "github.com/go-chi/chi/v5"
  11. "qctextbuilder/internal/buildsvc"
  12. "qctextbuilder/internal/config"
  13. "qctextbuilder/internal/domain"
  14. "qctextbuilder/internal/draftsvc"
  15. "qctextbuilder/internal/httpserver"
  16. "qctextbuilder/internal/httpserver/handlers"
  17. "qctextbuilder/internal/httpserver/views"
  18. "qctextbuilder/internal/llmruntime"
  19. "qctextbuilder/internal/logging"
  20. "qctextbuilder/internal/mapping"
  21. "qctextbuilder/internal/onboarding"
  22. "qctextbuilder/internal/polling"
  23. "qctextbuilder/internal/qcclient"
  24. "qctextbuilder/internal/store"
  25. "qctextbuilder/internal/store/memory"
  26. "qctextbuilder/internal/store/sqlite"
  27. "qctextbuilder/internal/templatesvc"
  28. )
  29. type App struct {
  30. server *httpserver.Server
  31. pollingSvc *polling.Service
  32. }
  33. func New(cfg config.Config) (*App, error) {
  34. logSetup := logging.Setup(cfg.LogLevel)
  35. logger := logSetup.Logger
  36. slog.SetDefault(logger)
  37. var (
  38. templateStore store.TemplateStore
  39. manifestStore store.ManifestStore
  40. buildStore store.BuildStore
  41. draftStore store.DraftStore
  42. settingsStore store.SettingsStore
  43. )
  44. driver := strings.ToLower(strings.TrimSpace(cfg.DBDriver))
  45. switch driver {
  46. case "", "sqlite":
  47. sqliteStore, err := sqlite.New(cfg.DBURL)
  48. if err != nil {
  49. return nil, fmt.Errorf("init sqlite store: %w", err)
  50. }
  51. templateStore = sqliteStore
  52. manifestStore = sqliteStore
  53. buildStore = sqliteStore
  54. draftStore = sqliteStore
  55. settingsStore = sqliteStore
  56. default:
  57. memStore := memory.New()
  58. templateStore = memStore
  59. manifestStore = memStore
  60. buildStore = memStore
  61. draftStore = memStore
  62. settingsStore = memStore
  63. }
  64. qc := qcclient.New(cfg.QCBaseURL, cfg.QCToken, 15*time.Second, logger)
  65. templateSvc := templatesvc.New(qc, templateStore, manifestStore)
  66. draftSvc := draftsvc.New(draftStore, templateStore, manifestStore)
  67. mappingSvc := mapping.New()
  68. buildSvc := buildsvc.New(qc, templateStore, manifestStore, buildStore, mappingSvc, time.Duration(cfg.PollTimeoutSeconds)*time.Second)
  69. providerRuntime := llmruntime.NewFactory(45 * time.Second)
  70. onboardSvc := onboarding.New(qc, templateStore, manifestStore).
  71. WithAISlotMapper(mapping.NewAISlotMapper(settingsStore, providerRuntime))
  72. suggestionGenerator := mapping.NewCompositeSuggestionGenerator(
  73. mapping.NewGooglePlacesSuggestionGenerator(settingsStore),
  74. mapping.NewCompositeSuggestionGenerator(
  75. mapping.NewProviderAwareSuggestionGenerator(settingsStore, providerRuntime),
  76. mapping.NewCompositeSuggestionGenerator(
  77. mapping.NewQCLLMSuggestionGenerator(qc),
  78. mapping.NewRuleBasedSuggestionGenerator(),
  79. ),
  80. ),
  81. )
  82. pollingSvc := polling.New(buildSvc, buildStore, time.Duration(cfg.PollIntervalSeconds)*time.Second, cfg.PollMaxConcurrent, logger)
  83. api := handlers.NewAPI(templateSvc, onboardSvc, draftSvc, buildSvc, logSetup.Recent)
  84. baseSettings := domain.AppSettings{
  85. QCBaseURL: cfg.QCBaseURL,
  86. QCBearerTokenEncrypted: cfg.QCToken,
  87. LanguageOutputMode: "EN",
  88. JobPollIntervalSeconds: cfg.PollIntervalSeconds,
  89. JobPollTimeoutSeconds: cfg.PollTimeoutSeconds,
  90. LLMActiveProvider: domain.DefaultLLMProvider(),
  91. LLMActiveModel: domain.NormalizeLLMModel(domain.DefaultLLMProvider(), ""),
  92. LLMTemperature: domain.DefaultLLMTemperature(),
  93. LLMMaxTokens: domain.DefaultLLMMaxTokens(),
  94. MasterPrompt: domain.SeedMasterPrompt,
  95. PromptBlocks: domain.DefaultPromptBlocks(),
  96. }
  97. if existing, err := settingsStore.GetSettings(context.Background()); err == nil && existing != nil {
  98. baseSettings.LLMActiveProvider = existing.LLMActiveProvider
  99. baseSettings.LLMActiveModel = existing.LLMActiveModel
  100. baseSettings.LLMBaseURL = existing.LLMBaseURL
  101. baseSettings.LLMTemperature = existing.LLMTemperature
  102. baseSettings.LLMMaxTokens = existing.LLMMaxTokens
  103. baseSettings.OpenAIAPIKeyEncrypted = existing.OpenAIAPIKeyEncrypted
  104. baseSettings.AnthropicAPIKeyEncrypted = existing.AnthropicAPIKeyEncrypted
  105. baseSettings.GoogleAPIKeyEncrypted = existing.GoogleAPIKeyEncrypted
  106. baseSettings.XAIAPIKeyEncrypted = existing.XAIAPIKeyEncrypted
  107. baseSettings.OllamaAPIKeyEncrypted = existing.OllamaAPIKeyEncrypted
  108. baseSettings.GooglePlacesAPIKeyEncrypted = existing.GooglePlacesAPIKeyEncrypted
  109. baseSettings.MasterPrompt = existing.MasterPrompt
  110. baseSettings.PromptBlocks = existing.PromptBlocks
  111. }
  112. _ = settingsStore.UpsertSettings(context.Background(), baseSettings)
  113. renderer, err := views.NewRenderer("web/templates/*.gohtml")
  114. if err != nil {
  115. return nil, fmt.Errorf("init renderer: %w", err)
  116. }
  117. ui := handlers.NewUI(templateSvc, onboardSvc, draftSvc, buildSvc, settingsStore, suggestionGenerator, cfg, renderer, logger)
  118. server := httpserver.New(cfg.HTTPAddr, logger, func(r chi.Router) {
  119. r.Get("/", ui.Home)
  120. r.Get("/settings", ui.Settings)
  121. r.Post("/settings/llm", ui.SaveLLMSettings)
  122. r.Post("/settings/llm/validate", ui.ValidateLLMSettings)
  123. r.Post("/settings/prompt", ui.SavePromptSettings)
  124. r.Get("/templates", ui.Templates)
  125. r.Post("/templates/sync", ui.SyncTemplates)
  126. r.Get("/templates/{id}", ui.TemplateDetail)
  127. r.Post("/templates/{id}/onboard", ui.OnboardTemplate)
  128. r.Post("/templates/{id}/fields", ui.UpdateTemplateFields)
  129. r.Get("/builds/new", ui.BuildNew)
  130. r.Post("/builds/drafts", ui.SaveDraft)
  131. r.Post("/builds/drafts/autofill", ui.AutofillDraft)
  132. r.Post("/builds", ui.CreateBuild)
  133. r.Get("/builds/{id}", ui.BuildDetail)
  134. r.Post("/builds/{id}/poll", ui.PollBuild)
  135. r.Post("/builds/{id}/fetch-editor-url", ui.FetchEditorURL)
  136. r.Get("/healthz", api.Health)
  137. r.Route("/api", func(r chi.Router) {
  138. r.Post("/templates/sync", api.SyncTemplates)
  139. r.Get("/templates", api.ListTemplates)
  140. r.Get("/templates/{id}", api.GetTemplateDetail)
  141. r.Post("/templates/{id}/onboard", api.OnboardTemplate)
  142. r.Put("/templates/{id}/fields", api.UpdateTemplateFields)
  143. r.Post("/drafts/intake", api.IntakeDraft)
  144. r.Get("/drafts", api.ListDrafts)
  145. r.Get("/drafts/{id}", api.GetDraft)
  146. r.Put("/drafts/{id}", api.UpdateDraft)
  147. r.Post("/site-builds", api.StartBuild)
  148. r.Get("/site-builds/{id}", api.GetBuild)
  149. r.Post("/site-builds/{id}/poll", api.PollBuildOnce)
  150. r.Post("/site-builds/{id}/fetch-editor-url", api.FetchBuildEditorURL)
  151. r.Get("/logs", api.ListLogs)
  152. })
  153. })
  154. return &App{server: server, pollingSvc: pollingSvc}, nil
  155. }
  156. func (a *App) Run(ctx context.Context) error {
  157. go func() {
  158. if err := a.pollingSvc.Start(ctx); err != nil {
  159. // polling is best-effort in milestone-2; request flow works without supervisor
  160. }
  161. }()
  162. errCh := make(chan error, 1)
  163. go func() {
  164. if err := a.server.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
  165. errCh <- fmt.Errorf("http run: %w", err)
  166. }
  167. close(errCh)
  168. }()
  169. select {
  170. case <-ctx.Done():
  171. shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  172. defer cancel()
  173. return a.server.Shutdown(shutdownCtx)
  174. case err := <-errCh:
  175. return err
  176. }
  177. }