umputun / umputun/newscope

llm.classification.feedback_examples never reaches either feedback query

Open
#46 0 comments 0 reactions 0 assignees View on GitHub

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

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:

  1. Wire it through both consumers. Add FeedbackExamples to scheduler.Params, FeedProcessorConfig and PreferenceManagerConfig, plumb it from main.go and 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.
  2. 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.
  3. Two settings, one per consumer, if both should be tunable without tying them together.
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.