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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user