llm.classification.feedback_examples never reaches either feedback query
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 53
- Forks
- 7
- PR merge metrics
- No merged PRs in 30d
Description
feedback_examples is parsed, defaulted and shown on the settings page, but neither consumer uses it: both ask the repository for 50 examples whatever the configuration says. Line references are against 35fcc394ad4e79b5302e4729f634052fd34a131a.
Mechanism
pkg/config/config.go:48:FeedbackExamplesis part ofClassificationConfig, described as "Number of recent feedback examples to include in prompt".pkg/config/config.go:174-176: it defaults to 10 when unset, matching the embedded schema atpkg/config/schema.json:8-12.- The value travels into
llm.NewClassifier(cfg.LLM)atcmd/newscope/main.go:117and is rendered on the settings page atserver/templates/settings.html:303, but nothing reads the field:grep -rn FeedbackExamples --include='*.go'outsidepkg/configreturns nothing. scheduler.Paramshas no field for it, and both consumers use literals instead:pkg/scheduler/feed_processor.go:198:GetRecentFeedback(ctx, "", 50)before every classification batch, feeding the examples into the article prompt atpkg/llm/classifier.go:309-318.pkg/scheduler/preference_manager.go:69-72:const feedbackExamples = 50with the comment "get more feedback examples for better learning (50 instead of 10)", feeding the separate summary prompts atpkg/llm/classifier.go:497-509andpkg/llm/classifier.go:582-594.
Reproduction
Drop this in pkg/scheduler/repro_test.go and run go test ./pkg/scheduler/ -run TestRepro_FeedbackExamplesIgnored:
package scheduler
import (
"context"
"os"
"path/filepath"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/newscope/pkg/config"
"github.com/umputun/newscope/pkg/domain"
"github.com/umputun/newscope/pkg/llm"
"github.com/umputun/newscope/pkg/scheduler/mocks"
)
func TestRepro_FeedbackExamplesIgnored(t *testing.T) {
ctx := context.Background()
cfgPath := filepath.Join(t.TempDir(), "config.yml")
require.NoError(t, os.WriteFile(cfgPath, []byte(`
server:
listen: ":8080"
timeout: "30s"
database:
dsn: ":memory:"
llm:
endpoint: "https://api.openai.com/v1"
api_key: "test-key"
model: "gpt-5"
classification:
feedback_examples: 5
schedule:
update_interval: "1m"
max_workers: 4
`), 0o600))
cfg, err := config.Load(cfgPath)
require.NoError(t, err)
require.Equal(t, 5, cfg.LLM.Classification.FeedbackExamples)
var mu sync.Mutex
var limits []int
classificationManager := &mocks.ClassificationManagerMock{
GetRecentFeedbackFunc: func(ctx context.Context, feedbackType string, limit int) ([]domain.FeedbackExample, error) {
mu.Lock()
defer mu.Unlock()
limits = append(limits, limit)
return []domain.FeedbackExample{{Title: "liked", Feedback: "like"}}, nil
},
GetTopicsFunc: func(ctx context.Context) ([]string, error) { return []string{}, nil },
GetFeedbackCountFunc: func(ctx context.Context) (int64, error) { return 1, nil },
}
settingManager := &mocks.SettingManagerMock{
GetSettingFunc: func(ctx context.Context, key string) (string, error) { return "", nil },
SetSettingFunc: func(ctx context.Context, key, value string) error { return nil },
}
classifier := &mocks.ClassifierMock{
ClassifyItemsFunc: func(ctx context.Context, req llm.ClassifyRequest) ([]domain.Classification, error) {
return []domain.Classification{}, nil
},
GeneratePreferenceSummaryFunc: func(ctx context.Context, feedback []domain.FeedbackExample) (string, error) {
return "summary", nil
},
}
// wired the way cmd/newscope/main.go wires it: Params has no field for feedback_examples
sched := NewScheduler(Params{
FeedManager: &mocks.FeedManagerMock{},
ItemManager: &mocks.ItemManagerMock{},
ClassificationManager: classificationManager,
SettingManager: settingManager,
Parser: &mocks.ParserMock{},
Extractor: &mocks.ExtractorMock{},
Classifier: classifier,
MaxWorkers: cfg.Schedule.MaxWorkers,
UpdateInterval: cfg.Schedule.UpdateInterval,
PreferenceSummaryThreshold: cfg.LLM.Classification.PreferenceSummaryThreshold,
})
sched.feedProcessor.ProcessBatch(ctx, []domain.Item{{ID: 1, GUID: "g1", Title: "item", Content: "content"}})
require.NoError(t, sched.UpdatePreferenceSummary(ctx))
mu.Lock()
defer mu.Unlock()
assert.Equal(t, []int{5, 5}, limits,
"both consumers should ask for the configured number of examples")
}
Relevant excerpt of the failure on master:
--- FAIL: TestRepro_FeedbackExamplesIgnored (0.00s)
Error: Not equal:
expected: []int{5, 5}
actual : []int{50, 50}
Messages: both consumers should ask for the configured number of examples
The two entries are the classification batch and the preference summary respectively.
Impact
Low. config.yml and the README both ship feedback_examples: 50, which matches the hardcoded value, so the effect shows up for anyone who edits the setting or leaves it out: a config omitting the key gets 50 where the loader and the embedded schema say 10, lowering it to save prompt space changes nothing, and raising it past 50 silently caps at 50. The settings page reports the configured number as though it were in force. Beyond prompt size, the number of examples is part of the context the model classifies against, so a wrong count can shift scores and summaries as well.
Options
The comment in preference_manager.go reads as a deliberate choice, so the open question is what the setting is meant to control:
- Wire it through both consumers. Add
FeedbackExamplestoscheduler.Params,FeedProcessorConfigandPreferenceManagerConfig, plumb it frommain.goand replace both literals. Installs without the key in their config would drop from an effective 50 to the documented default of 10, so the loader default would want to move to 50 to keep classification behaviour as it is now, at the cost of the settings page showing 50 where it used to show 10. - Wire it through classification only and leave the preference summary on its own constant, since the two differ in cost and cadence: article prompts pay on every batch, summaries are rebuilt rarely. This preserves the intent in the comment; the constant deserves a comment saying it is deliberately independent of the setting.
- Two settings, one per consumer, if both should be tunable without tying them together.
- Drop the setting from the config struct,
config.yml, the README, the settings template, the embedded schema and the test configs, and keep both constants as they are.
If the value is wired into either query, it needs validating as greater than zero in validateRequiredFields: the jsonschema tag carries no minimum, the embedded schema is parsed but never applied, zero is silently turned into the default, and a negative value would reach SQLite as a negative LIMIT, which returns every row.
I would take option 2, with the loader default raised to 50 so classification behaviour does not change, and the preference-summary constant documented as intentionally fixed.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with pkg/config/config.go and the scheduler wiring in cmd/newscope/main.go, then trace the consumers in pkg/scheduler/feed_processor.go and preference_manager.go into pkg/llm/classifier.go. Run the reproduction test in pkg/scheduler/repro_test.go and inspect related scheduler tests. Done means the intended setting scope is decided, configured values reach the selected queries, and validation and tests cover defaults and invalid values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- ai, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100