| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import type { AutoTuneParams, ContextType } from './autotune' |
| |
|
| | |
| |
|
| | export interface ResponseHeuristics { |
| | responseLength: number |
| | repetitionScore: number |
| | averageSentenceLength: number |
| | vocabularyDiversity: number |
| | } |
| |
|
| | export interface FeedbackRecord { |
| | messageId: string |
| | timestamp: number |
| | contextType: ContextType |
| | model: string |
| | persona: string |
| | params: AutoTuneParams |
| | rating: 1 | -1 |
| | heuristics: ResponseHeuristics |
| | } |
| |
|
| | export interface LearnedProfile { |
| | contextType: ContextType |
| | sampleCount: number |
| | positiveCount: number |
| | negativeCount: number |
| | positiveParams: AutoTuneParams |
| | negativeParams: AutoTuneParams |
| | adjustments: Partial<AutoTuneParams> |
| | lastUpdated: number |
| | } |
| |
|
| | export interface FeedbackState { |
| | history: FeedbackRecord[] |
| | learnedProfiles: Record<ContextType, LearnedProfile> |
| | } |
| |
|
| | |
| |
|
| | const EMA_ALPHA = 0.3 |
| | const MAX_HISTORY = 500 |
| | const MIN_SAMPLES_TO_APPLY = 3 |
| | const MAX_LEARNED_WEIGHT = 0.5 |
| | const SAMPLES_FOR_MAX_WEIGHT = 20 |
| |
|
| | |
| | const NEUTRAL_PARAMS: AutoTuneParams = { |
| | temperature: 0.7, |
| | top_p: 0.9, |
| | top_k: 50, |
| | frequency_penalty: 0.2, |
| | presence_penalty: 0.2, |
| | repetition_penalty: 1.1 |
| | } |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| | export function computeHeuristics(response: string): ResponseHeuristics { |
| | const responseLength = response.length |
| |
|
| | |
| | const repetitionScore = computeRepetitionScore(response) |
| |
|
| | |
| | const sentences = response.split(/[.!?]+/).filter(s => s.trim().length > 0) |
| | const averageSentenceLength = sentences.length > 0 |
| | ? sentences.reduce((sum, s) => sum + s.trim().split(/\s+/).length, 0) / sentences.length |
| | : 0 |
| |
|
| | |
| | const words = response.toLowerCase().split(/\s+/).filter(w => w.length > 0) |
| | const uniqueWords = new Set(words) |
| | const vocabularyDiversity = words.length > 0 ? uniqueWords.size / words.length : 1 |
| |
|
| | return { |
| | responseLength, |
| | repetitionScore, |
| | averageSentenceLength, |
| | vocabularyDiversity |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function computeRepetitionScore(text: string): number { |
| | const words = text.toLowerCase().split(/\s+/).filter(w => w.length > 0) |
| | if (words.length < 6) return 0 |
| |
|
| | const trigrams = new Map<string, number>() |
| | let totalTrigrams = 0 |
| |
|
| | for (let i = 0; i <= words.length - 3; i++) { |
| | const trigram = `${words[i]} ${words[i + 1]} ${words[i + 2]}` |
| | trigrams.set(trigram, (trigrams.get(trigram) || 0) + 1) |
| | totalTrigrams++ |
| | } |
| |
|
| | if (totalTrigrams === 0) return 0 |
| |
|
| | |
| | let repeatedCount = 0 |
| | trigrams.forEach((count) => { |
| | if (count > 1) { |
| | repeatedCount += count - 1 |
| | } |
| | }) |
| |
|
| | return Math.min(repeatedCount / totalTrigrams, 1.0) |
| | } |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | export function createInitialFeedbackState(): FeedbackState { |
| | const contexts: ContextType[] = ['code', 'creative', 'analytical', 'conversational', 'chaotic'] |
| |
|
| | const learnedProfiles: Record<string, LearnedProfile> = {} |
| | for (const ctx of contexts) { |
| | learnedProfiles[ctx] = { |
| | contextType: ctx, |
| | sampleCount: 0, |
| | positiveCount: 0, |
| | negativeCount: 0, |
| | positiveParams: { ...NEUTRAL_PARAMS }, |
| | negativeParams: { ...NEUTRAL_PARAMS }, |
| | adjustments: {}, |
| | lastUpdated: 0 |
| | } |
| | } |
| |
|
| | return { |
| | history: [], |
| | learnedProfiles: learnedProfiles as Record<ContextType, LearnedProfile> |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export function processFeedback( |
| | state: FeedbackState, |
| | record: FeedbackRecord |
| | ): FeedbackState { |
| | |
| | const newHistory = [...state.history, record] |
| | if (newHistory.length > MAX_HISTORY) { |
| | newHistory.splice(0, newHistory.length - MAX_HISTORY) |
| | } |
| |
|
| | |
| | const profile = { ...state.learnedProfiles[record.contextType] } |
| | profile.sampleCount++ |
| | profile.lastUpdated = Date.now() |
| |
|
| | if (record.rating === 1) { |
| | |
| | profile.positiveCount++ |
| | profile.positiveParams = emaUpdate(profile.positiveParams, record.params, EMA_ALPHA) |
| | } else { |
| | |
| | profile.negativeCount++ |
| | profile.negativeParams = emaUpdate(profile.negativeParams, record.params, EMA_ALPHA) |
| | } |
| |
|
| | |
| | profile.adjustments = computeAdjustments(profile) |
| |
|
| | const newProfiles = { |
| | ...state.learnedProfiles, |
| | [record.contextType]: profile |
| | } |
| |
|
| | return { |
| | history: newHistory, |
| | learnedProfiles: newProfiles |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function emaUpdate( |
| | current: AutoTuneParams, |
| | observation: AutoTuneParams, |
| | alpha: number |
| | ): AutoTuneParams { |
| | const inv = 1 - alpha |
| | return { |
| | temperature: current.temperature * inv + observation.temperature * alpha, |
| | top_p: current.top_p * inv + observation.top_p * alpha, |
| | top_k: Math.round(current.top_k * inv + observation.top_k * alpha), |
| | frequency_penalty: current.frequency_penalty * inv + observation.frequency_penalty * alpha, |
| | presence_penalty: current.presence_penalty * inv + observation.presence_penalty * alpha, |
| | repetition_penalty: current.repetition_penalty * inv + observation.repetition_penalty * alpha |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | function computeAdjustments(profile: LearnedProfile): Partial<AutoTuneParams> { |
| | |
| | if (profile.positiveCount < 1 || profile.negativeCount < 1) { |
| | |
| | if (profile.positiveCount >= MIN_SAMPLES_TO_APPLY) { |
| | return computeDeltaFromNeutral(profile.positiveParams, 0.5) |
| | } |
| | return {} |
| | } |
| |
|
| | const adj: Partial<AutoTuneParams> = {} |
| | const keys: (keyof AutoTuneParams)[] = [ |
| | 'temperature', 'top_p', 'top_k', |
| | 'frequency_penalty', 'presence_penalty', 'repetition_penalty' |
| | ] |
| |
|
| | for (const key of keys) { |
| | const posDelta = profile.positiveParams[key] - NEUTRAL_PARAMS[key] |
| | const negDelta = profile.negativeParams[key] - NEUTRAL_PARAMS[key] |
| | |
| | const adjustment = (posDelta - negDelta) * 0.5 |
| | |
| | if (Math.abs(adjustment) > 0.01) { |
| | adj[key] = adjustment |
| | } |
| | } |
| |
|
| | return adj |
| | } |
| |
|
| | |
| | |
| | |
| | function computeDeltaFromNeutral( |
| | positiveParams: AutoTuneParams, |
| | scale: number |
| | ): Partial<AutoTuneParams> { |
| | const adj: Partial<AutoTuneParams> = {} |
| | const keys: (keyof AutoTuneParams)[] = [ |
| | 'temperature', 'top_p', 'top_k', |
| | 'frequency_penalty', 'presence_penalty', 'repetition_penalty' |
| | ] |
| |
|
| | for (const key of keys) { |
| | const delta = (positiveParams[key] - NEUTRAL_PARAMS[key]) * scale |
| | if (Math.abs(delta) > 0.01) { |
| | adj[key] = delta |
| | } |
| | } |
| |
|
| | return adj |
| | } |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function applyLearnedAdjustments( |
| | baseParams: AutoTuneParams, |
| | contextType: ContextType, |
| | learnedProfiles: Record<ContextType, LearnedProfile> |
| | ): { params: AutoTuneParams; applied: boolean; note: string } { |
| | const profile = learnedProfiles[contextType] |
| |
|
| | if (!profile || profile.sampleCount < MIN_SAMPLES_TO_APPLY || Object.keys(profile.adjustments).length === 0) { |
| | return { params: baseParams, applied: false, note: '' } |
| | } |
| |
|
| | |
| | const weight = Math.min( |
| | (profile.sampleCount / SAMPLES_FOR_MAX_WEIGHT) * MAX_LEARNED_WEIGHT, |
| | MAX_LEARNED_WEIGHT |
| | ) |
| |
|
| | const adjusted = { ...baseParams } |
| | const appliedKeys: string[] = [] |
| |
|
| | for (const [key, delta] of Object.entries(profile.adjustments)) { |
| | const k = key as keyof AutoTuneParams |
| | if (delta !== undefined) { |
| | adjusted[k] = (adjusted[k] as number) + (delta as number) * weight |
| | appliedKeys.push(key) |
| | } |
| | } |
| |
|
| | const note = `Learned: ${appliedKeys.length} params adjusted (${profile.sampleCount} samples, ${Math.round(weight * 100)}% weight)` |
| |
|
| | return { params: adjusted, applied: true, note } |
| | } |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | export function getFeedbackStats(state: FeedbackState): { |
| | totalFeedback: number |
| | positiveRate: number |
| | contextBreakdown: Record<ContextType, { total: number; positive: number; negative: number; hasLearned: boolean }> |
| | oldestRecord: number | null |
| | newestRecord: number | null |
| | } { |
| | const contexts: ContextType[] = ['code', 'creative', 'analytical', 'conversational', 'chaotic'] |
| | const totalFeedback = state.history.length |
| | const positiveCount = state.history.filter(r => r.rating === 1).length |
| |
|
| | const contextBreakdown = {} as Record<ContextType, { total: number; positive: number; negative: number; hasLearned: boolean }> |
| | for (const ctx of contexts) { |
| | const profile = state.learnedProfiles[ctx] |
| | contextBreakdown[ctx] = { |
| | total: profile.sampleCount, |
| | positive: profile.positiveCount, |
| | negative: profile.negativeCount, |
| | hasLearned: profile.sampleCount >= MIN_SAMPLES_TO_APPLY && Object.keys(profile.adjustments).length > 0 |
| | } |
| | } |
| |
|
| | return { |
| | totalFeedback, |
| | positiveRate: totalFeedback > 0 ? positiveCount / totalFeedback : 0, |
| | contextBreakdown, |
| | oldestRecord: state.history.length > 0 ? state.history[0].timestamp : null, |
| | newestRecord: state.history.length > 0 ? state.history[state.history.length - 1].timestamp : null |
| | } |
| | } |
| |
|