feat(server): hot-reload config store and reloadable infra clients
- Replace single Load() with a watching Store that re-reads config files
(and config.local.yaml) and fans out a ReloadEvent with a per-field diff
so consumers can decide whether the change is hot-applicable or requires
a process restart.
- Wrap llm, retrieval, vector store, and object storage clients in
Reloadable* shells so the bootstrap can swap their underlying impls when
config changes without re-instantiating handlers.
- Make jwt.Manager and ops TokenIssuer mutable under a lock so secrets and
TTLs can be rotated live; thread default plan code through a setter on
the ops AdminUserService.
- Wire ConfigStore through bootstrap and every cmd/main.go, scheduler /
worker / tenant-api / ops-api start the watcher; services and handlers
take a config.Provider so they always read current values for things
like generation.stream_enabled, scheduler dispatch, retrieval, etc.
- Switch shared/config decoding off viper to a Kratos-derived runtime
package so env placeholders (\${VAR:default}) resolve consistently and
the same source machinery powers both the loader and the watcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,15 +30,14 @@ const (
|
||||
)
|
||||
|
||||
type ArticleImitationService struct {
|
||||
pool *pgxpool.Pool
|
||||
llm llm.Client
|
||||
rabbitMQ *rabbitmq.Client
|
||||
knowledge *KnowledgeService
|
||||
streams *stream.GenerationHub
|
||||
streamEnabled bool
|
||||
articleTimeout time.Duration
|
||||
maxOutputTokens int64
|
||||
cache sharedcache.Cache
|
||||
pool *pgxpool.Pool
|
||||
llm llm.Client
|
||||
rabbitMQ *rabbitmq.Client
|
||||
knowledge *KnowledgeService
|
||||
streams *stream.GenerationHub
|
||||
cfg generationRuntimeConfig
|
||||
configProvider runtimeConfigProvider
|
||||
cache sharedcache.Cache
|
||||
}
|
||||
|
||||
type GenerateImitationRequest struct {
|
||||
@@ -86,20 +85,13 @@ func NewArticleImitationService(
|
||||
cfg config.GenerationConfig,
|
||||
llmMaxOutputTokens int64,
|
||||
) *ArticleImitationService {
|
||||
articleTimeout := cfg.ArticleTimeout
|
||||
if articleTimeout <= 0 {
|
||||
articleTimeout = 8 * time.Minute
|
||||
}
|
||||
|
||||
return &ArticleImitationService{
|
||||
pool: pool,
|
||||
llm: llmClient,
|
||||
rabbitMQ: rabbitMQClient,
|
||||
knowledge: knowledge,
|
||||
streams: streams,
|
||||
streamEnabled: cfg.StreamEnabled,
|
||||
articleTimeout: articleTimeout,
|
||||
maxOutputTokens: llmMaxOutputTokens,
|
||||
pool: pool,
|
||||
llm: llmClient,
|
||||
rabbitMQ: rabbitMQClient,
|
||||
knowledge: knowledge,
|
||||
streams: streams,
|
||||
cfg: generationRuntimeFromConfig(cfg, config.LLMConfig{MaxOutputTokens: llmMaxOutputTokens}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +100,23 @@ func (s *ArticleImitationService) WithCache(c sharedcache.Cache) *ArticleImitati
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ArticleImitationService) WithConfigProvider(provider runtimeConfigProvider) *ArticleImitationService {
|
||||
s.configProvider = provider
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ArticleImitationService) runtimeConfig() generationRuntimeConfig {
|
||||
if s != nil && s.configProvider != nil {
|
||||
if cfg := s.configProvider.Current(); cfg != nil {
|
||||
return generationRuntimeFromConfig(cfg.Generation, cfg.LLM)
|
||||
}
|
||||
}
|
||||
if s == nil {
|
||||
return generationRuntimeFromConfig(config.GenerationConfig{}, config.LLMConfig{})
|
||||
}
|
||||
return s.cfg
|
||||
}
|
||||
|
||||
func (s *ArticleImitationService) Generate(ctx context.Context, req GenerateImitationRequest) (*GenerateImitationResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
if err := s.llm.Validate(); err != nil {
|
||||
@@ -234,7 +243,8 @@ func (s *ArticleImitationService) Generate(ctx context.Context, req GenerateImit
|
||||
return nil, response.ErrServiceUnavailable(50310, "generation_queue_unavailable", "generation queue is busy, please retry")
|
||||
}
|
||||
|
||||
if s.streamEnabled && s.streams != nil {
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil {
|
||||
s.streams.Start(articleID, taskID, initialTitle)
|
||||
}
|
||||
|
||||
@@ -337,17 +347,18 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
|
||||
}
|
||||
|
||||
prompt := buildImitationGenerationPrompt(job.InputParams, sourceContent, knowledgePrompt)
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
req := llm.GenerateRequest{
|
||||
Prompt: prompt,
|
||||
Timeout: s.articleTimeout,
|
||||
MaxOutputTokens: s.maxOutputTokens,
|
||||
Timeout: runtimeCfg.ArticleTimeout,
|
||||
MaxOutputTokens: runtimeCfg.MaxOutputTokens,
|
||||
}
|
||||
if job.WebSearch.Enabled {
|
||||
req.WebSearch = &job.WebSearch
|
||||
}
|
||||
|
||||
result, err := s.llm.Generate(ctx, req, func(delta string) {
|
||||
if s.streamEnabled && s.streams != nil && delta != "" {
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil && delta != "" {
|
||||
s.streams.AppendDelta(job.ArticleID, job.TaskID, title, delta)
|
||||
}
|
||||
})
|
||||
@@ -429,7 +440,7 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
|
||||
defer cancel()
|
||||
invalidateArticleCaches(cleanupCtx, s.cache, job.TenantID, &job.ArticleID)
|
||||
|
||||
if s.streamEnabled && s.streams != nil {
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil {
|
||||
s.streams.Complete(job.ArticleID, job.TaskID, title, content)
|
||||
}
|
||||
}
|
||||
@@ -490,7 +501,8 @@ func (s *ArticleImitationService) failGeneration(ctx context.Context, job articl
|
||||
invalidateArticleCaches(cleanupCtx, s.cache, job.TenantID, &job.ArticleID)
|
||||
}
|
||||
|
||||
if s.streamEnabled && s.streams != nil && job.ArticleID > 0 {
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil && job.ArticleID > 0 {
|
||||
s.streams.Fail(job.ArticleID, job.TaskID, title, errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ type ArticleSelectionOptimizeService struct {
|
||||
pool *pgxpool.Pool
|
||||
llm llm.Client
|
||||
defaultMaxOutTokens int64
|
||||
configProvider runtimeConfigProvider
|
||||
cache sharedcache.Cache
|
||||
}
|
||||
|
||||
@@ -59,6 +60,11 @@ func (s *ArticleSelectionOptimizeService) WithCache(c sharedcache.Cache) *Articl
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ArticleSelectionOptimizeService) WithConfigProvider(provider runtimeConfigProvider) *ArticleSelectionOptimizeService {
|
||||
s.configProvider = provider
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ArticleSelectionOptimizeService) ValidateRequest(
|
||||
ctx context.Context,
|
||||
articleID int64,
|
||||
@@ -174,6 +180,11 @@ func (s *ArticleSelectionOptimizeService) ensureArticleEditable(
|
||||
|
||||
func (s *ArticleSelectionOptimizeService) resolveMaxOutputTokens(selectedText string) int64 {
|
||||
limit := s.defaultMaxOutTokens
|
||||
if s != nil && s.configProvider != nil {
|
||||
if cfg := s.configProvider.Current(); cfg != nil {
|
||||
limit = cfg.LLM.MaxOutputTokens
|
||||
}
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 1200
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
type BrandService struct {
|
||||
pool *pgxpool.Pool
|
||||
monitoringPool *pgxpool.Pool
|
||||
limitsMu sync.RWMutex
|
||||
limits sharedconfig.BrandLibraryConfig
|
||||
auditLogs *auditlog.AsyncWriter
|
||||
cache sharedcache.Cache
|
||||
@@ -39,6 +41,24 @@ func (s *BrandService) WithCache(c sharedcache.Cache) *BrandService {
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *BrandService) UpdateConfig(limits sharedconfig.BrandLibraryConfig) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.limitsMu.Lock()
|
||||
s.limits = limits
|
||||
s.limitsMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *BrandService) currentLimits() sharedconfig.BrandLibraryConfig {
|
||||
if s == nil {
|
||||
return sharedconfig.BrandLibraryConfig{}
|
||||
}
|
||||
s.limitsMu.RLock()
|
||||
defer s.limitsMu.RUnlock()
|
||||
return s.limits
|
||||
}
|
||||
|
||||
// --- Brand CRUD ---
|
||||
|
||||
type BrandRequest struct {
|
||||
@@ -809,11 +829,12 @@ func (s *BrandService) loadBrandLibrarySummary(ctx context.Context, tenantID int
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxBrands := s.limits.BrandLimitForPlan(plan.PlanCode)
|
||||
limits := s.currentLimits()
|
||||
maxBrands := limits.BrandLimitForPlan(plan.PlanCode)
|
||||
if plan.MaxBrands > 0 {
|
||||
maxBrands = plan.MaxBrands
|
||||
}
|
||||
maxKeywords := s.limits.MaxKeywords
|
||||
maxKeywords := limits.MaxKeywords
|
||||
|
||||
return &BrandLibrarySummaryResponse{
|
||||
PlanCode: plan.PlanCode,
|
||||
@@ -824,15 +845,16 @@ func (s *BrandService) loadBrandLibrarySummary(ctx context.Context, tenantID int
|
||||
MaxKeywords: maxKeywords,
|
||||
UsedKeywords: usage.KeywordCount,
|
||||
RemainingKeywords: maxInt(maxKeywords-usage.KeywordCount, 0),
|
||||
MaxQuestionsPerKeyword: s.limits.MaxQuestionsPerKeyword,
|
||||
MaxQuestionsPerKeyword: limits.MaxQuestionsPerKeyword,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BrandService) loadBrandLibraryPlan(ctx context.Context, tenantID int64) (*brandLibraryPlan, error) {
|
||||
limits := s.currentLimits()
|
||||
plan := &brandLibraryPlan{
|
||||
PlanCode: "free",
|
||||
PlanName: "",
|
||||
MaxBrands: s.limits.BrandLimitForPlan("free"),
|
||||
MaxBrands: limits.BrandLimitForPlan("free"),
|
||||
}
|
||||
|
||||
var quotaPolicyJSON []byte
|
||||
|
||||
@@ -107,10 +107,12 @@ func HandleClaimedGenerationTaskFailure(
|
||||
title = promptRuleGeneratingTitlePlaceholder
|
||||
}
|
||||
|
||||
if task.TaskType == "template" && templateService != nil && templateService.streamEnabled {
|
||||
if task.TaskType == "template" && templateService != nil &&
|
||||
templateService.runtimeConfig().StreamEnabled && templateService.streams != nil {
|
||||
templateService.streams.Fail(task.ArticleID, task.ID, title, errMsg)
|
||||
}
|
||||
if task.TaskType == "custom_generation" && promptRuleService != nil && promptRuleService.streamEnabled {
|
||||
if task.TaskType == "custom_generation" && promptRuleService != nil &&
|
||||
promptRuleService.runtimeConfig().StreamEnabled && promptRuleService.streams != nil {
|
||||
promptRuleService.streams.Fail(task.ArticleID, task.ID, title, errMsg)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,24 +35,18 @@ const (
|
||||
)
|
||||
|
||||
type KnowledgeService struct {
|
||||
pool *pgxpool.Pool
|
||||
provider retrieval.Provider
|
||||
store retrieval.VectorStore
|
||||
objectStorage objectstorage.Client
|
||||
llmClient llm.Client
|
||||
logger *zap.Logger
|
||||
chunkSize int
|
||||
chunkOverlap int
|
||||
embeddingBatchSize int
|
||||
recallLimit int
|
||||
rerankTopN int
|
||||
arkBaseURL string
|
||||
arkAPIKey string
|
||||
urlMarkdownModel string
|
||||
httpClient *http.Client
|
||||
jobs chan knowledgeParseJob
|
||||
cleanupMu sync.Mutex
|
||||
cleanupInFlight map[string]struct{}
|
||||
pool *pgxpool.Pool
|
||||
provider retrieval.Provider
|
||||
store retrieval.VectorStore
|
||||
objectStorage objectstorage.Client
|
||||
llmClient llm.Client
|
||||
logger *zap.Logger
|
||||
cfg knowledgeRuntimeConfig
|
||||
configProvider runtimeConfigProvider
|
||||
httpClient *http.Client
|
||||
jobs chan knowledgeParseJob
|
||||
cleanupMu sync.Mutex
|
||||
cleanupInFlight map[string]struct{}
|
||||
}
|
||||
|
||||
type knowledgeParseJob struct {
|
||||
@@ -169,50 +163,16 @@ func NewKnowledgeService(
|
||||
queueSize int,
|
||||
workerCount int,
|
||||
) *KnowledgeService {
|
||||
chunkSize := cfg.ChunkSize
|
||||
if chunkSize <= 0 {
|
||||
chunkSize = 900
|
||||
}
|
||||
chunkOverlap := cfg.ChunkOverlap
|
||||
if chunkOverlap < 0 {
|
||||
chunkOverlap = 120
|
||||
}
|
||||
batchSize := cfg.EmbeddingBatchSize
|
||||
if batchSize <= 0 {
|
||||
batchSize = 16
|
||||
}
|
||||
recallLimit := cfg.RecallLimit
|
||||
if recallLimit <= 0 {
|
||||
recallLimit = 12
|
||||
}
|
||||
rerankTopN := cfg.RerankTopN
|
||||
if rerankTopN <= 0 {
|
||||
rerankTopN = 6
|
||||
}
|
||||
|
||||
svc := &KnowledgeService{
|
||||
pool: pool,
|
||||
provider: provider,
|
||||
store: store,
|
||||
objectStorage: objectStorage,
|
||||
llmClient: llmClient,
|
||||
logger: logger,
|
||||
chunkSize: chunkSize,
|
||||
chunkOverlap: chunkOverlap,
|
||||
embeddingBatchSize: batchSize,
|
||||
recallLimit: recallLimit,
|
||||
rerankTopN: rerankTopN,
|
||||
arkBaseURL: strings.TrimSpace(llmCfg.BaseURL),
|
||||
arkAPIKey: strings.TrimSpace(llmCfg.APIKey),
|
||||
urlMarkdownModel: strings.TrimSpace(llmCfg.KnowledgeURLModel),
|
||||
httpClient: &http.Client{Timeout: defaultKnowledgeURLParseTimeout},
|
||||
cleanupInFlight: make(map[string]struct{}),
|
||||
}
|
||||
if svc.urlMarkdownModel == "" {
|
||||
svc.urlMarkdownModel = strings.TrimSpace(llmCfg.Model)
|
||||
}
|
||||
if svc.arkBaseURL == "" {
|
||||
svc.arkBaseURL = defaultKnowledgeArkBaseURL
|
||||
pool: pool,
|
||||
provider: provider,
|
||||
store: store,
|
||||
objectStorage: objectStorage,
|
||||
llmClient: llmClient,
|
||||
logger: logger,
|
||||
cfg: knowledgeRuntimeFromConfig(cfg, llmCfg),
|
||||
httpClient: &http.Client{Timeout: defaultKnowledgeURLParseTimeout},
|
||||
cleanupInFlight: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
if workerCount > 0 {
|
||||
@@ -229,6 +189,23 @@ func NewKnowledgeService(
|
||||
return svc
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) WithConfigProvider(provider runtimeConfigProvider) *KnowledgeService {
|
||||
s.configProvider = provider
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) runtimeConfig() knowledgeRuntimeConfig {
|
||||
if s != nil && s.configProvider != nil {
|
||||
if cfg := s.configProvider.Current(); cfg != nil {
|
||||
return knowledgeRuntimeFromConfig(cfg.Retrieval, cfg.LLM)
|
||||
}
|
||||
}
|
||||
if s == nil {
|
||||
return knowledgeRuntimeFromConfig(config.RetrievalConfig{}, config.LLMConfig{})
|
||||
}
|
||||
return s.cfg
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
@@ -781,11 +758,12 @@ func (s *KnowledgeService) ResolveContext(
|
||||
return nil, response.ErrServiceUnavailable(50353, "embedding_failed", "embedding vector is empty")
|
||||
}
|
||||
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
searchPoints, err := s.store.Search(ctx, retrieval.SearchRequest{
|
||||
Vector: vectors[0],
|
||||
TenantID: tenantID,
|
||||
GroupIDs: searchGroupIDs,
|
||||
Limit: s.recallLimit,
|
||||
Limit: runtimeCfg.RecallLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, response.ErrServiceUnavailable(50354, "qdrant_search_failed", err.Error())
|
||||
@@ -815,9 +793,9 @@ func (s *KnowledgeService) ResolveContext(
|
||||
documents = append(documents, candidate.Text)
|
||||
}
|
||||
|
||||
reranked, err := s.provider.Rerank(ctx, query, documents, minInt(s.rerankTopN, len(documents)))
|
||||
reranked, err := s.provider.Rerank(ctx, query, documents, minInt(runtimeCfg.RerankTopN, len(documents)))
|
||||
if err != nil {
|
||||
selected := selectKnowledgeCandidateSnippets(candidates, s.rerankTopN)
|
||||
selected := selectKnowledgeCandidateSnippets(candidates, runtimeCfg.RerankTopN)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
|
||||
baseContext.Snippets = selected
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, selected)
|
||||
@@ -825,7 +803,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
return baseContext, nil
|
||||
}
|
||||
|
||||
selected := selectKnowledgeRerankedSnippets(candidates, reranked, s.rerankTopN)
|
||||
selected := selectKnowledgeRerankedSnippets(candidates, reranked, runtimeCfg.RerankTopN)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
|
||||
baseContext.Snippets = selected
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, selected)
|
||||
@@ -1606,7 +1584,8 @@ func (s *KnowledgeService) validateIngestionDependencies(sourceType string) erro
|
||||
}
|
||||
}
|
||||
if sourceType == "website" {
|
||||
if strings.TrimSpace(s.arkAPIKey) == "" {
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
if strings.TrimSpace(runtimeCfg.ArkAPIKey) == "" {
|
||||
return response.ErrServiceUnavailable(50358, "knowledge_webpage_parser_unavailable", "ark api key is not configured")
|
||||
}
|
||||
if err := s.llmClient.Validate(); err != nil {
|
||||
@@ -1761,7 +1740,8 @@ func (s *KnowledgeService) executeParseJob(ctx context.Context, job knowledgePar
|
||||
chunkSource = strings.TrimSpace(parsedContent.Text)
|
||||
}
|
||||
|
||||
chunks := splitKnowledgeTextIntoChunks(chunkSource, s.chunkSize, s.chunkOverlap)
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
chunks := splitKnowledgeTextIntoChunks(chunkSource, runtimeCfg.ChunkSize, runtimeCfg.ChunkOverlap)
|
||||
if len(chunks) == 0 {
|
||||
err = errors.New("knowledge content is empty after parsing")
|
||||
_ = s.markKnowledgeIngestionFailed(context.Background(), job.TenantID, job.ItemID, job.ParseTaskID, err)
|
||||
@@ -2371,9 +2351,10 @@ func (s *KnowledgeService) listActiveKnowledgeItemIDs(
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) embedChunks(ctx context.Context, chunks []string) ([][]float64, error) {
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
results := make([][]float64, 0, len(chunks))
|
||||
for start := 0; start < len(chunks); start += s.embeddingBatchSize {
|
||||
end := start + s.embeddingBatchSize
|
||||
for start := 0; start < len(chunks); start += runtimeCfg.EmbeddingBatchSize {
|
||||
end := start + runtimeCfg.EmbeddingBatchSize
|
||||
if end > len(chunks) {
|
||||
end = len(chunks)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ func (s *KnowledgeService) parseWebsiteKnowledge(ctx context.Context, rawURL str
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) executeLinkReader(ctx context.Context, rawURL string) (*arkWebDataItem, error) {
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(s.arkBaseURL), "/")
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(runtimeCfg.ArkBaseURL), "/")
|
||||
if baseURL == "" {
|
||||
baseURL = defaultKnowledgeArkBaseURL
|
||||
}
|
||||
@@ -102,7 +103,7 @@ func (s *KnowledgeService) executeLinkReader(ctx context.Context, rawURL string)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create webpage parser request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+s.arkAPIKey)
|
||||
req.Header.Set("Authorization", "Bearer "+runtimeCfg.ArkAPIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
@@ -146,9 +147,10 @@ func (s *KnowledgeService) formatWebsiteMarkdown(ctx context.Context, title, con
|
||||
}
|
||||
|
||||
sections := make([]string, 0, len(chunks)+1)
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
for index, chunk := range chunks {
|
||||
result, err := s.llmClient.Generate(ctx, llm.GenerateRequest{
|
||||
Model: s.urlMarkdownModel,
|
||||
Model: runtimeCfg.URLMarkdownModel,
|
||||
Prompt: prompts.KnowledgeWebsiteMarkdownPrompt(title, rawURL, chunk, index, len(chunks)),
|
||||
Timeout: defaultKnowledgeURLMarkdownTimeout,
|
||||
MaxOutputTokens: defaultKnowledgeURLMarkdownOutputToken,
|
||||
|
||||
@@ -19,10 +19,10 @@ const kolAssistReferenceContentMaxRunes = 6000
|
||||
var kolAssistURLPattern = regexp.MustCompile(`https?://[^\s]+`)
|
||||
|
||||
type kolAssistURLResolver struct {
|
||||
arkBaseURL string
|
||||
arkAPIKey string
|
||||
httpClient *http.Client
|
||||
logger *zap.Logger
|
||||
cfg config.LLMConfig
|
||||
configProvider runtimeConfigProvider
|
||||
httpClient *http.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type kolAssistReferenceMaterial struct {
|
||||
@@ -38,17 +38,25 @@ func newKolAssistURLResolver(llmCfg config.LLMConfig, logger *zap.Logger) *kolAs
|
||||
}
|
||||
|
||||
return &kolAssistURLResolver{
|
||||
arkBaseURL: strings.TrimRight(baseURL, "/"),
|
||||
arkAPIKey: strings.TrimSpace(llmCfg.APIKey),
|
||||
cfg: config.LLMConfig{
|
||||
BaseURL: baseURL,
|
||||
APIKey: strings.TrimSpace(llmCfg.APIKey),
|
||||
},
|
||||
httpClient: &http.Client{Timeout: defaultKnowledgeURLParseTimeout},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *kolAssistURLResolver) extract(ctx context.Context, rawURL string) (*kolAssistReferenceMaterial, error) {
|
||||
if r == nil || r.httpClient == nil || strings.TrimSpace(r.arkAPIKey) == "" {
|
||||
llmCfg := r.runtimeConfig()
|
||||
apiKey := strings.TrimSpace(llmCfg.APIKey)
|
||||
if r == nil || r.httpClient == nil || apiKey == "" {
|
||||
return nil, fmt.Errorf("url resolver is unavailable")
|
||||
}
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(llmCfg.BaseURL), "/")
|
||||
if baseURL == "" {
|
||||
baseURL = defaultKnowledgeArkBaseURL
|
||||
}
|
||||
|
||||
body, err := json.Marshal(arkToolExecuteRequest{
|
||||
ActionName: "LinkReader",
|
||||
@@ -62,11 +70,11 @@ func (r *kolAssistURLResolver) extract(ctx context.Context, rawURL string) (*kol
|
||||
return nil, fmt.Errorf("marshal webpage parser request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.arkBaseURL+"/tools/execute", bytes.NewReader(body))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/tools/execute", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create webpage parser request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+r.arkAPIKey)
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := r.httpClient.Do(req)
|
||||
@@ -108,6 +116,26 @@ func (r *kolAssistURLResolver) extract(ctx context.Context, rawURL string) (*kol
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *kolAssistURLResolver) withConfigProvider(provider runtimeConfigProvider) *kolAssistURLResolver {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.configProvider = provider
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *kolAssistURLResolver) runtimeConfig() config.LLMConfig {
|
||||
if r != nil && r.configProvider != nil {
|
||||
if cfg := r.configProvider.Current(); cfg != nil {
|
||||
return cfg.LLM
|
||||
}
|
||||
}
|
||||
if r == nil {
|
||||
return config.LLMConfig{BaseURL: defaultKnowledgeArkBaseURL}
|
||||
}
|
||||
return r.cfg
|
||||
}
|
||||
|
||||
func (s *KolAssistService) enrichGenerateDescription(ctx context.Context, description string) string {
|
||||
description = strings.TrimSpace(description)
|
||||
urls := extractKolAssistURLs(description)
|
||||
|
||||
@@ -85,6 +85,13 @@ func (s *KolAssistService) WithCache(c sharedcache.Cache) *KolAssistService {
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *KolAssistService) WithConfigProvider(provider runtimeConfigProvider) *KolAssistService {
|
||||
if s != nil && s.urlResolver != nil {
|
||||
s.urlResolver.withConfigProvider(provider)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *KolAssistService) Submit(ctx context.Context, actor auth.Actor, req AssistRequest) (string, error) {
|
||||
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
|
||||
if err != nil {
|
||||
|
||||
@@ -482,9 +482,10 @@ func (s *MonitoringService) generateDailyMonitoringTasksForBrand(
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadDailyMonitoringPlans(ctx context.Context) ([]monitoringDailyPlan, error) {
|
||||
freeBrandLimit := normalizeMonitoringDailyBrandLimit(s.brandLibraryConfig.FreeBrandLimit)
|
||||
paidBrandLimit := normalizeMonitoringDailyBrandLimit(s.brandLibraryConfig.PaidBrandLimit)
|
||||
questionLimit := monitoringDailyQuestionLimit(s.brandLibraryConfig)
|
||||
_, brandLibraryConfig := s.currentConfig()
|
||||
freeBrandLimit := normalizeMonitoringDailyBrandLimit(brandLibraryConfig.FreeBrandLimit)
|
||||
paidBrandLimit := normalizeMonitoringDailyBrandLimit(brandLibraryConfig.PaidBrandLimit)
|
||||
questionLimit := monitoringDailyQuestionLimit(brandLibraryConfig)
|
||||
supportedPlatforms := monitoringPlatformIDs(defaultMonitoringPlatforms)
|
||||
|
||||
candidates, err := s.loadDailyMonitoringPrimaryClientPlans(ctx, freeBrandLimit, paidBrandLimit, questionLimit, supportedPlatforms)
|
||||
|
||||
@@ -85,17 +85,18 @@ func (s *MonitoringService) shouldUseDesktopTasksExecution(tenantID int64) bool
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
if s.desktopTasksRolloutPercentage <= 0 {
|
||||
rolloutPercentage, _ := s.currentConfig()
|
||||
if rolloutPercentage <= 0 {
|
||||
return false
|
||||
}
|
||||
if s.desktopTasksRolloutPercentage >= 100 {
|
||||
if rolloutPercentage >= 100 {
|
||||
return true
|
||||
}
|
||||
mod := tenantID % 100
|
||||
if mod < 0 {
|
||||
mod = -mod
|
||||
}
|
||||
return int(mod) < s.desktopTasksRolloutPercentage
|
||||
return int(mod) < rolloutPercentage
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadMonitorDesktopTaskSpecs(
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -43,6 +44,7 @@ type MonitoringService struct {
|
||||
rabbitMQ *rabbitmq.Client
|
||||
redis *goredis.Client
|
||||
logger *zap.Logger
|
||||
configMu sync.RWMutex
|
||||
desktopTasksRolloutPercentage int
|
||||
brandLibraryConfig config.BrandLibraryConfig
|
||||
}
|
||||
@@ -71,6 +73,25 @@ func (s *MonitoringService) WithRedis(redis *goredis.Client) *MonitoringService
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *MonitoringService) UpdateConfig(dispatchCfg config.MonitoringDispatchConfig, brandLibraryCfg config.BrandLibraryConfig) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.configMu.Lock()
|
||||
s.desktopTasksRolloutPercentage = dispatchCfg.DesktopTasksRolloutPercentage
|
||||
s.brandLibraryConfig = brandLibraryCfg
|
||||
s.configMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *MonitoringService) currentConfig() (int, config.BrandLibraryConfig) {
|
||||
if s == nil {
|
||||
return 0, config.BrandLibraryConfig{}
|
||||
}
|
||||
s.configMu.RLock()
|
||||
defer s.configMu.RUnlock()
|
||||
return s.desktopTasksRolloutPercentage, s.brandLibraryConfig
|
||||
}
|
||||
|
||||
type MonitoringDashboardCompositeResponse struct {
|
||||
Overview MonitoringOverview `json:"overview"`
|
||||
Runtime MonitoringDashboardRuntime `json:"runtime"`
|
||||
|
||||
@@ -23,17 +23,16 @@ import (
|
||||
)
|
||||
|
||||
type PromptRuleGenerationService struct {
|
||||
pool *pgxpool.Pool
|
||||
llm llm.Client
|
||||
rabbitMQ *rabbitmq.Client
|
||||
knowledge *KnowledgeService
|
||||
streams *stream.GenerationHub
|
||||
streamEnabled bool
|
||||
articleTimeout time.Duration
|
||||
maxOutputTokens int64
|
||||
cache sharedcache.Cache
|
||||
publishJobs *PublishJobService
|
||||
logger *zap.Logger
|
||||
pool *pgxpool.Pool
|
||||
llm llm.Client
|
||||
rabbitMQ *rabbitmq.Client
|
||||
knowledge *KnowledgeService
|
||||
streams *stream.GenerationHub
|
||||
cfg generationRuntimeConfig
|
||||
configProvider runtimeConfigProvider
|
||||
cache sharedcache.Cache
|
||||
publishJobs *PublishJobService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type promptRuleGenerationJob struct {
|
||||
@@ -119,20 +118,13 @@ func NewPromptRuleGenerationService(
|
||||
cfg config.GenerationConfig,
|
||||
llmMaxOutputTokens int64,
|
||||
) *PromptRuleGenerationService {
|
||||
articleTimeout := cfg.ArticleTimeout
|
||||
if articleTimeout <= 0 {
|
||||
articleTimeout = 8 * time.Minute
|
||||
}
|
||||
|
||||
svc := &PromptRuleGenerationService{
|
||||
pool: pool,
|
||||
llm: llmClient,
|
||||
rabbitMQ: rabbitMQClient,
|
||||
knowledge: knowledge,
|
||||
streams: streams,
|
||||
streamEnabled: cfg.StreamEnabled,
|
||||
articleTimeout: articleTimeout,
|
||||
maxOutputTokens: llmMaxOutputTokens,
|
||||
pool: pool,
|
||||
llm: llmClient,
|
||||
rabbitMQ: rabbitMQClient,
|
||||
knowledge: knowledge,
|
||||
streams: streams,
|
||||
cfg: generationRuntimeFromConfig(cfg, config.LLMConfig{MaxOutputTokens: llmMaxOutputTokens}),
|
||||
}
|
||||
|
||||
return svc
|
||||
@@ -153,6 +145,23 @@ func (s *PromptRuleGenerationService) WithLogger(logger *zap.Logger) *PromptRule
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) WithConfigProvider(provider runtimeConfigProvider) *PromptRuleGenerationService {
|
||||
s.configProvider = provider
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) runtimeConfig() generationRuntimeConfig {
|
||||
if s != nil && s.configProvider != nil {
|
||||
if cfg := s.configProvider.Current(); cfg != nil {
|
||||
return generationRuntimeFromConfig(cfg.Generation, cfg.LLM)
|
||||
}
|
||||
}
|
||||
if s == nil {
|
||||
return generationRuntimeFromConfig(config.GenerationConfig{}, config.LLMConfig{})
|
||||
}
|
||||
return s.cfg
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) GenerateFromRule(ctx context.Context, req GenerateFromRuleRequest) (*GenerateFromRuleResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
extra := clonePromptRuleExtraParams(req.ExtraParams)
|
||||
@@ -440,7 +449,8 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
|
||||
return nil, response.ErrServiceUnavailable(50310, "generation_queue_unavailable", "generation queue is busy, please retry")
|
||||
}
|
||||
|
||||
if s.streamEnabled && s.streams != nil {
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil {
|
||||
s.streams.Start(articleID, taskID, promptRuleGeneratingTitlePlaceholder)
|
||||
}
|
||||
|
||||
@@ -505,17 +515,18 @@ func (s *PromptRuleGenerationService) executeGeneration(ctx context.Context, job
|
||||
StartedAt: &now,
|
||||
})
|
||||
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
req := llm.GenerateRequest{
|
||||
Prompt: job.Prompt,
|
||||
Timeout: s.articleTimeout,
|
||||
MaxOutputTokens: s.maxOutputTokens,
|
||||
Timeout: runtimeCfg.ArticleTimeout,
|
||||
MaxOutputTokens: runtimeCfg.MaxOutputTokens,
|
||||
}
|
||||
if job.WebSearch.Enabled {
|
||||
req.WebSearch = &job.WebSearch
|
||||
}
|
||||
|
||||
result, err := s.llm.Generate(ctx, req, func(delta string) {
|
||||
if s.streamEnabled && delta != "" {
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil && delta != "" {
|
||||
s.streams.AppendDelta(job.ArticleID, job.TaskID, title, delta)
|
||||
}
|
||||
})
|
||||
@@ -596,7 +607,7 @@ func (s *PromptRuleGenerationService) executeGeneration(ctx context.Context, job
|
||||
defer cancel()
|
||||
invalidateArticleCaches(cleanupCtx, s.cache, job.TenantID, &job.ArticleID)
|
||||
|
||||
if s.streamEnabled {
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil {
|
||||
s.streams.Complete(job.ArticleID, job.TaskID, title, content)
|
||||
}
|
||||
|
||||
@@ -721,7 +732,8 @@ func (s *PromptRuleGenerationService) failGeneration(ctx context.Context, job pr
|
||||
_ = quotaRepo.RefundReservation(cleanupCtx, job.ReservationID, job.TenantID)
|
||||
invalidateArticleCaches(cleanupCtx, s.cache, job.TenantID, &job.ArticleID)
|
||||
|
||||
if s.streamEnabled {
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil {
|
||||
s.streams.Fail(job.ArticleID, job.TaskID, failureTitle, errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config"
|
||||
)
|
||||
|
||||
const defaultArticleGenerationTimeout = 8 * time.Minute
|
||||
|
||||
type runtimeConfigProvider = config.Provider
|
||||
|
||||
type generationRuntimeConfig struct {
|
||||
StreamEnabled bool
|
||||
ArticleTimeout time.Duration
|
||||
MaxOutputTokens int64
|
||||
}
|
||||
|
||||
type knowledgeRuntimeConfig struct {
|
||||
ChunkSize int
|
||||
ChunkOverlap int
|
||||
EmbeddingBatchSize int
|
||||
RecallLimit int
|
||||
RerankTopN int
|
||||
ArkBaseURL string
|
||||
ArkAPIKey string
|
||||
URLMarkdownModel string
|
||||
}
|
||||
|
||||
type generationStreamDisabledConfigProvider struct {
|
||||
base config.Provider
|
||||
}
|
||||
|
||||
func NewGenerationStreamDisabledConfigProvider(base config.Provider) config.Provider {
|
||||
return generationStreamDisabledConfigProvider{base: base}
|
||||
}
|
||||
|
||||
func (p generationStreamDisabledConfigProvider) Current() *config.Config {
|
||||
if p.base == nil {
|
||||
return nil
|
||||
}
|
||||
cfg := p.base.Current()
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
next := *cfg
|
||||
next.Generation.StreamEnabled = false
|
||||
return &next
|
||||
}
|
||||
|
||||
func generationRuntimeFromConfig(generation config.GenerationConfig, llmCfg config.LLMConfig) generationRuntimeConfig {
|
||||
articleTimeout := generation.ArticleTimeout
|
||||
if articleTimeout <= 0 {
|
||||
articleTimeout = defaultArticleGenerationTimeout
|
||||
}
|
||||
return generationRuntimeConfig{
|
||||
StreamEnabled: generation.StreamEnabled,
|
||||
ArticleTimeout: articleTimeout,
|
||||
MaxOutputTokens: llmCfg.MaxOutputTokens,
|
||||
}
|
||||
}
|
||||
|
||||
func knowledgeRuntimeFromConfig(retrievalCfg config.RetrievalConfig, llmCfg config.LLMConfig) knowledgeRuntimeConfig {
|
||||
chunkSize := retrievalCfg.ChunkSize
|
||||
if chunkSize <= 0 {
|
||||
chunkSize = 900
|
||||
}
|
||||
chunkOverlap := retrievalCfg.ChunkOverlap
|
||||
if chunkOverlap < 0 {
|
||||
chunkOverlap = 120
|
||||
}
|
||||
batchSize := retrievalCfg.EmbeddingBatchSize
|
||||
if batchSize <= 0 {
|
||||
batchSize = 16
|
||||
}
|
||||
recallLimit := retrievalCfg.RecallLimit
|
||||
if recallLimit <= 0 {
|
||||
recallLimit = 12
|
||||
}
|
||||
rerankTopN := retrievalCfg.RerankTopN
|
||||
if rerankTopN <= 0 {
|
||||
rerankTopN = 6
|
||||
}
|
||||
|
||||
model := strings.TrimSpace(llmCfg.KnowledgeURLModel)
|
||||
if model == "" {
|
||||
model = strings.TrimSpace(llmCfg.Model)
|
||||
}
|
||||
baseURL := strings.TrimSpace(llmCfg.BaseURL)
|
||||
if baseURL == "" {
|
||||
baseURL = defaultKnowledgeArkBaseURL
|
||||
}
|
||||
|
||||
return knowledgeRuntimeConfig{
|
||||
ChunkSize: chunkSize,
|
||||
ChunkOverlap: chunkOverlap,
|
||||
EmbeddingBatchSize: batchSize,
|
||||
RecallLimit: recallLimit,
|
||||
RerankTopN: rerankTopN,
|
||||
ArkBaseURL: baseURL,
|
||||
ArkAPIKey: strings.TrimSpace(llmCfg.APIKey),
|
||||
URLMarkdownModel: model,
|
||||
}
|
||||
}
|
||||
@@ -22,16 +22,15 @@ import (
|
||||
)
|
||||
|
||||
type TemplateService struct {
|
||||
pool *pgxpool.Pool
|
||||
templates repository.TemplateRepository
|
||||
llm llm.Client
|
||||
rabbitMQ *rabbitmq.Client
|
||||
knowledge *KnowledgeService
|
||||
streams *stream.GenerationHub
|
||||
streamEnabled bool
|
||||
articleTimeout time.Duration
|
||||
maxOutputTokens int64
|
||||
cache sharedcache.Cache
|
||||
pool *pgxpool.Pool
|
||||
templates repository.TemplateRepository
|
||||
llm llm.Client
|
||||
rabbitMQ *rabbitmq.Client
|
||||
knowledge *KnowledgeService
|
||||
streams *stream.GenerationHub
|
||||
cfg generationRuntimeConfig
|
||||
configProvider runtimeConfigProvider
|
||||
cache sharedcache.Cache
|
||||
}
|
||||
|
||||
type generationJob struct {
|
||||
@@ -58,21 +57,14 @@ func NewTemplateService(
|
||||
cfg config.GenerationConfig,
|
||||
llmMaxOutputTokens int64,
|
||||
) *TemplateService {
|
||||
articleTimeout := cfg.ArticleTimeout
|
||||
if articleTimeout <= 0 {
|
||||
articleTimeout = 8 * time.Minute
|
||||
}
|
||||
|
||||
svc := &TemplateService{
|
||||
pool: pool,
|
||||
templates: templates,
|
||||
llm: llmClient,
|
||||
rabbitMQ: rabbitMQClient,
|
||||
knowledge: knowledge,
|
||||
streams: streams,
|
||||
streamEnabled: cfg.StreamEnabled,
|
||||
articleTimeout: articleTimeout,
|
||||
maxOutputTokens: llmMaxOutputTokens,
|
||||
pool: pool,
|
||||
templates: templates,
|
||||
llm: llmClient,
|
||||
rabbitMQ: rabbitMQClient,
|
||||
knowledge: knowledge,
|
||||
streams: streams,
|
||||
cfg: generationRuntimeFromConfig(cfg, config.LLMConfig{MaxOutputTokens: llmMaxOutputTokens}),
|
||||
}
|
||||
|
||||
return svc
|
||||
@@ -83,6 +75,23 @@ func (s *TemplateService) WithCache(c sharedcache.Cache) *TemplateService {
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *TemplateService) WithConfigProvider(provider runtimeConfigProvider) *TemplateService {
|
||||
s.configProvider = provider
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *TemplateService) runtimeConfig() generationRuntimeConfig {
|
||||
if s != nil && s.configProvider != nil {
|
||||
if cfg := s.configProvider.Current(); cfg != nil {
|
||||
return generationRuntimeFromConfig(cfg.Generation, cfg.LLM)
|
||||
}
|
||||
}
|
||||
if s == nil {
|
||||
return generationRuntimeFromConfig(config.GenerationConfig{}, config.LLMConfig{})
|
||||
}
|
||||
return s.cfg
|
||||
}
|
||||
|
||||
type TemplateListItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Scope string `json:"scope"`
|
||||
@@ -397,7 +406,8 @@ func (s *TemplateService) Generate(ctx context.Context, templateID int64, req Ge
|
||||
return nil, response.ErrServiceUnavailable(50310, "generation_queue_unavailable", "generation queue is busy, please retry")
|
||||
}
|
||||
|
||||
if s.streamEnabled {
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil {
|
||||
s.streams.Start(articleID, taskID, initialTitle)
|
||||
}
|
||||
|
||||
@@ -467,16 +477,17 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
|
||||
}
|
||||
|
||||
prompt := buildGenerationPrompt(job.TemplateKey, job.TemplateName, job.PromptTemplate, job.Params, knowledgePrompt)
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
generateReq := llm.GenerateRequest{
|
||||
Prompt: prompt,
|
||||
Timeout: s.articleTimeout,
|
||||
MaxOutputTokens: s.maxOutputTokens,
|
||||
Timeout: runtimeCfg.ArticleTimeout,
|
||||
MaxOutputTokens: runtimeCfg.MaxOutputTokens,
|
||||
}
|
||||
if job.WebSearch.Enabled {
|
||||
generateReq.WebSearch = &job.WebSearch
|
||||
}
|
||||
result, err := s.llm.Generate(ctx, generateReq, func(delta string) {
|
||||
if s.streamEnabled && delta != "" {
|
||||
if runtimeCfg.StreamEnabled && delta != "" {
|
||||
s.streams.AppendDelta(job.ArticleID, job.TaskID, title, delta)
|
||||
}
|
||||
})
|
||||
@@ -557,7 +568,7 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
|
||||
defer cancel()
|
||||
invalidateArticleCaches(cleanupCtx, s.cache, job.TenantID, &job.ArticleID)
|
||||
|
||||
if s.streamEnabled {
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil {
|
||||
s.streams.Complete(job.ArticleID, job.TaskID, title, content)
|
||||
}
|
||||
}
|
||||
@@ -602,7 +613,8 @@ func (s *TemplateService) failGeneration(ctx context.Context, job generationJob,
|
||||
_ = quotaRepo.RefundReservation(cleanupCtx, job.ReservationID, job.TenantID)
|
||||
invalidateArticleCaches(cleanupCtx, s.cache, job.TenantID, &job.ArticleID)
|
||||
|
||||
if s.streamEnabled {
|
||||
runtimeCfg := s.runtimeConfig()
|
||||
if runtimeCfg.StreamEnabled && s.streams != nil {
|
||||
s.streams.Fail(job.ArticleID, job.TaskID, title, errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user