SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
12 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
5.6 KB · 133 lines typescript
Raw Blame History
1import { clamp01, jaccard, tokenize, type ActionScore, type AgentMode, type ObservedEntity, type PageState, type SemanticAction } from "@src/shared";2import type { WorldModel } from "./WorldModel.ts";34/**5 * Information Gain Engine (§26–§27):6 *   gain = novelty × relevance × expected_entity_yield × confidence × source_quality ÷ cost7 * with penalties for already-seen / duplicate / loop / low-quality / low-relevance.8 */9export interface GainContext {10  goal: string;11  mode: AgentMode;12  world: WorldModel;13  state: PageState;14  recentActionTypes: string[]; // last N action types15  recentUrls: string[];16  stepsWithoutNewEntities: number;17  /** Learned expectations: action type → average new entities produced (from the platform model). */18  learnedYield?: Record<string, number>;19  /** Labels (type + target) of actions that failed in the last few steps — retrying them blindly is a loop in disguise. */20  recentFailures?: Set<string>;21}2223export function actionKey(a: SemanticAction): string {24  return `${a.type}:${a.target_url ?? a.query ?? a.label}`;25}2627const TYPE_YIELD: Record<string, number> = {28  OPEN_CHANNEL: 0.8,29  OPEN_PROFILE: 0.8,30  OPEN_PAGE: 0.75,31  OPEN_VIDEO: 0.65,32  OPEN_POST: 0.65,33  OPEN_COMMENTS: 0.55,34  OPEN_ENTITY: 0.6,35  SCROLL_DOWN: 0.6,36  SEARCH: 0.85,37  EXPAND: 0.4,38  PLAY_VIDEO: 0.3,39  SCROLL_UP: 0.05,40  BACK: 0.2,41  RETURN_TO_FEED: 0.3,42  FILTER: 0.4,43  WAIT_FOR_CONTENT: 0.1,44  END_SESSION: 0.0,45  FORWARD: 0.1,46  PAUSE_VIDEO: 0.0,47  COLLAPSE: 0.0,48};4950export function relevanceOf(text: string | undefined, goal: string): number {51  if (!goal.trim()) return 0.6;52  if (!text) return 0.35;53  const g = tokenize(goal);54  const t = tokenize(text);55  if (g.size === 0 || t.size === 0) return 0.4;56  let hits = 0;57  for (const tok of g) if (t.has(tok) || [...t].some((x) => x.startsWith(tok.slice(0, 5)) && tok.length > 4)) hits++;58  const coverage = hits / g.size;59  return clamp01(0.3 + 0.7 * coverage + 0.2 * jaccard(g, t));60}6162function entityOf(ctx: GainContext, a: SemanticAction): ObservedEntity | undefined {63  return a.target_ref ? ctx.state.entities.find((e) => e.ref === a.target_ref) : undefined;64}6566export function scoreActions(ctx: GainContext, actions: SemanticAction[]): ActionScore[] {67  const scores: ActionScore[] = [];68  const currentRel = relevanceOf(ctx.state.summary_text.slice(0, 1500), ctx.goal);69  for (const a of actions) {70    const penalties: string[] = [];71    const e = entityOf(ctx, a);72    let novelty = 0.5;73    let relevance = 0.5;74    let confidence = 0.7;75    let source_quality = 0.7;76    let yieldExp = ctx.learnedYield?.[a.type] !== undefined ? clamp01(ctx.learnedYield[a.type]! / 10) : TYPE_YIELD[a.type] ?? 0.3;7778    if (e) {79      novelty = ctx.world.novelty(e);80      relevance = relevanceOf(`${e.name ?? ""} ${e.text ?? ""} ${e.author ?? ""}`, ctx.goal);81      confidence = Math.max(0, ...e.provenance.map((p) => p.confidence));82      source_quality = e.provenance.some((p) => p.surface === "network") && e.provenance.some((p) => p.surface === "dom") ? 0.95 : 0.75;83      if (ctx.world.isVisited(e)) penalties.push("already_seen");84      if (/^(navigation|header|sidebar)/.test(e.context ?? "") && ctx.mode !== "learn") penalties.push("low_quality_source");85      if (!e.name && !e.text) penalties.push("low_confidence_entity");86      // Profiles/channels are hubs: raise yield when the goal is about people/orgs.87      if ((e.type === "channel" || e.type === "profile") && /personalit|people|person|creator|influenc|chaîne|channel|profil/i.test(ctx.goal)) yieldExp = Math.min(1, yieldExp + 0.15);88    } else {89      switch (a.type) {90        case "SCROLL_DOWN":91          relevance = currentRel;92          novelty = ctx.stepsWithoutNewEntities > 2 ? 0.2 : 0.6;93          if (ctx.recentActionTypes.slice(-4).every((t) => t === "SCROLL_DOWN") && ctx.recentActionTypes.length >= 4) penalties.push("navigation_loop");94          break;95        case "SEARCH":96          relevance = 0.9;97          novelty = ctx.recentActionTypes.includes("SEARCH") ? 0.15 : 0.9;98          if (ctx.state.classification.page_type === "SEARCH_RESULTS") penalties.push("already_seen");99          break;100        case "EXPAND":101          relevance = currentRel;102          novelty = 0.5;103          break;104        case "PLAY_VIDEO":105          relevance = currentRel;106          novelty = ctx.state.classification.page_type === "VIDEO_DETAIL" ? 0.6 : 0.3;107          break;108        case "BACK":109        case "RETURN_TO_FEED":110          relevance = 0.4;111          novelty = ctx.stepsWithoutNewEntities > 1 ? 0.6 : 0.2;112          if (ctx.recentActionTypes.slice(-2).includes(a.type)) penalties.push("navigation_loop");113          break;114        case "END_SESSION":115          relevance = 0.1;116          novelty = ctx.stepsWithoutNewEntities > 6 ? 0.9 : 0.05;117          break;118        default:119          break;120      }121    }122    if (ctx.mode === "observe" && a.type !== "SCROLL_DOWN" && a.type !== "WAIT_FOR_CONTENT" && a.type !== "END_SESSION") penalties.push("mode_observe_no_navigation");123    if (relevance < 0.35 && e) penalties.push("low_relevance");124    if (ctx.recentFailures?.has(actionKey(a))) penalties.push("recently_failed");125126    let gain = (novelty * relevance * yieldExp * confidence * source_quality) / Math.max(0.25, a.cost);127    for (const p of penalties) gain *= p === "already_seen" ? 0.05 : p === "navigation_loop" ? 0.15 : p === "mode_observe_no_navigation" ? 0.02 : p === "recently_failed" ? 0.1 : 0.5;128129    scores.push({ action_id: a.id, novelty, relevance, expected_entity_yield: yieldExp, confidence, source_quality, cost: a.cost, penalties, information_gain: gain });130  }131  return scores.sort((x, y) => y.information_gain - x.information_gain);132}133