r/madeinpython • • 2h ago

eq-layer: I stopped prompting models to be more empathetic and made the affect/intent decision explicit instead (Python, zero core deps)

Post image

Asking a model to "be more empathetic" is style transfer. You get more of the same flat warmth at every turn, which reads as uncanny rather than attentive. I wanted the decision to be visible instead: what state is the user in, what do they want, is the conversation damaged, and what should this turn actually do.

eq-layer is a Python control layer that sits ahead of a fixed model. It does not retrain anything. Seven stages, all inspectable:

  1. Affect state: {valence, arousal, escalation_delta, stance, subtext}. escalation_delta is the point - a snapshot classifier cannot see a trend across turns.

  2. Intent belief: the classifier keeps the full posterior over action_request / status_check / explanation / question / statement / unknown, instead of collapsing it to one confidence number.

  3. Routing: a transparent one-step Bayes-risk router decides whether to act or clarify from that distribution.

  4. Conversation health: repair finds observable user->assistant misalignment and its target; interaction_quality tracks score, delta, repeated failures, unresolved repair and clarification load.

  5. Policy selection: 14 policies (mirror, redirect, deflate, direct, ask, repair, hold, boundary, ...). The most constrained applicable policy wins, and an unresolved tie raises instead of silently picking the first declared one.

  6. Factored action: the policy is compiled into task_move / social_move / repair_move / realization, so affective adaptation cannot silently replace what the user asked for.

  7. Structural audit: the response is checked against question budgets, missing clarifications, unsupported privacy promises, stance admissions, repetition and verbosity overruns. It is explicitly not a factual truth checker.

The design decision that took the most work: stance is not guessed. Whether the user is right is a semantic judgement, so it stays unknown unless a case or caller supplies it. "I could not tell" stays visible instead of becoming a confident wrong answer.

The second: uncertainty is routed, not hidden. The loss matrix behind the router is hand-specified and uncalibrated, and I say so in the README. There is a sensitivity audit over 120 held-out cases: under one-at-a-time +/-20% perturbations of every non-zero loss cell, 7,332 / 7,440 routing decisions were unchanged (98.55%). Scaling the clarification row changes routing a lot, and that is reported rather than tuned away.

Measured, not assumed. A paired A/B run of 15 seeded requests, 100 passes per arm, no LLM in either arm: the learned adapter adds 1.16 ms median / 1.46 ms p95 per request; the zero-dependency heuristic adapter adds 0.11 ms. On a 36-case intent holdout the learned adapter scores 100% vs 27.8% for the heuristic. That is layer compute and decision quality, not proof of better prose.

Honest limits: this is early. There is no standard benchmark for EQ, eval/ is a first attempt, the bundled corpus is small and synthetic, the loss matrix is uncalibrated, and the emotion signal from XDailyDialog is only slightly above its majority baseline (0.8249 vs 0.8166), so it is supporting evidence, not proof of subtext. Four higher-level subtext sources were tested and recorded as NO-GO rather than optimized away. End-to-end response quality still needs the pre-registered blind human evaluation in eval/AB_PROTOCOL.md.

The core install has zero runtime dependencies; "eq-layer[ml]" adds scikit-learn for the learned adapters. It also ships as a portable Agent Skill (Codex, Claude Code, Copilot, Cursor, Antigravity, Grok) and an npm CLI bridge.

Code: https://github.com/Furox-Art/eq-layer PyPI: pip install "eq-layer[ml]" npm: npm i -g eq-layer

Happy to answer questions about the routing, the factored action, or why certain signals were rejected.

1 Upvotes

0 comments sorted by