eq-layer 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {eq_layer-0.2.3 → eq_layer-0.2.4}/PKG-INFO +2 -2
  2. {eq_layer-0.2.3 → eq_layer-0.2.4}/README.md +1 -1
  3. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/affect.py +9 -5
  4. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/response_experiment.py +26 -2
  5. {eq_layer-0.2.3 → eq_layer-0.2.4}/pyproject.toml +1 -1
  6. {eq_layer-0.2.3 → eq_layer-0.2.4}/.gitignore +0 -0
  7. {eq_layer-0.2.3 → eq_layer-0.2.4}/LICENSE +0 -0
  8. {eq_layer-0.2.3 → eq_layer-0.2.4}/THIRD_PARTY_DATA.md +0 -0
  9. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/__init__.py +0 -0
  10. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/_deps.py +0 -0
  11. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/_version.py +0 -0
  12. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/ab_eval.py +0 -0
  13. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/actions.py +0 -0
  14. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/auditor.py +0 -0
  15. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/cli.py +0 -0
  16. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/coarse_discourse.py +0 -0
  17. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/data/intent_train.jsonl +0 -0
  18. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/dbdc3.py +0 -0
  19. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/dialogbank_iso.py +0 -0
  20. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/dialogue_reference.py +0 -0
  21. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/intent.py +0 -0
  22. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/intent_belief.py +0 -0
  23. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/interaction.py +0 -0
  24. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/model_state.py +0 -0
  25. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/policies.py +0 -0
  26. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/preferences.py +0 -0
  27. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/steer.py +0 -0
  28. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/textnorm.py +0 -0
  29. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/tracker.py +0 -0
  30. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/trained_affect.py +0 -0
  31. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/trained_breakdown.py +0 -0
  32. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/trained_disagreement.py +0 -0
  33. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/trained_intent.py +0 -0
  34. {eq_layer-0.2.3 → eq_layer-0.2.4}/eq_layer/trained_subtext.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: eq-layer
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Inspectable intent, affect, repair, uncertainty, and factored-action control for LLM responses
5
5
  Project-URL: Homepage, https://github.com/Furox-Art/eq-layer
6
6
  Project-URL: Repository, https://github.com/Furox-Art/eq-layer
@@ -51,7 +51,7 @@ So this repo does not retrain the model. It adds a layer:
51
51
  1. **Affect state** — `{valence, arousal, escalation_delta, stance, subtext}` as an explicit intermediate representation, tracked across turns. `escalation_delta` is the point: snapshot classification cannot see a trend.
52
52
  2. **Intent belief state** — the learned classifier preserves the full posterior over `action_request/status_check/explanation/question/statement/unknown` instead of collapsing uncertainty to one confidence number. A transparent one-step Bayes-risk router chooses whether to act or clarify from that distribution. This is POMDP-inspired uncertainty handling, not a full POMDP.
53
53
  3. **Conversation health state** — `repair` identifies observable user→assistant misalignment and its target; `interaction_quality` tracks structural conversation failure separately from emotion using current score, delta, repeated failures, unresolved repair, and clarification load.
54
- 4. **Policy selection** — affect and intent jointly choose a response-policy family (`mirror`, `direct`, `repair`, `hold`, `boundary`, ...). Selection is a discrete decision, not generation.
54
+ 4. **Policy selection** — affect and intent jointly choose a response-policy family (`mirror`, `validate_redirect`, `direct`, `ask`, `deflate`, `repair`, `hold`, `boundary`). Selection is a discrete decision, not generation.
55
55
  5. **Factored action compilation** — the selected policy is split into orthogonal controls: `task_move`, `social_move`, `repair_move`, and `realization`. The task is driven primarily by user intent, so affective adaptation cannot silently replace what the user actually asked for.
56
56
  6. **Steering** — the factored action, source policy, repair state, interaction quality, and canonical request are injected into the decode path so the control decision actually lands in the tokens.
57
57
  7. **Structural response audit** — the generated response is checked against inspectable control contracts such as question budget, required clarification, unsupported privacy promises, unknown-stance admissions, repair repetition, and low-verbosity overruns. This is not a factual truth checker.
@@ -16,7 +16,7 @@ So this repo does not retrain the model. It adds a layer:
16
16
  1. **Affect state** — `{valence, arousal, escalation_delta, stance, subtext}` as an explicit intermediate representation, tracked across turns. `escalation_delta` is the point: snapshot classification cannot see a trend.
17
17
  2. **Intent belief state** — the learned classifier preserves the full posterior over `action_request/status_check/explanation/question/statement/unknown` instead of collapsing uncertainty to one confidence number. A transparent one-step Bayes-risk router chooses whether to act or clarify from that distribution. This is POMDP-inspired uncertainty handling, not a full POMDP.
18
18
  3. **Conversation health state** — `repair` identifies observable user→assistant misalignment and its target; `interaction_quality` tracks structural conversation failure separately from emotion using current score, delta, repeated failures, unresolved repair, and clarification load.
19
- 4. **Policy selection** — affect and intent jointly choose a response-policy family (`mirror`, `direct`, `repair`, `hold`, `boundary`, ...). Selection is a discrete decision, not generation.
19
+ 4. **Policy selection** — affect and intent jointly choose a response-policy family (`mirror`, `validate_redirect`, `direct`, `ask`, `deflate`, `repair`, `hold`, `boundary`). Selection is a discrete decision, not generation.
20
20
  5. **Factored action compilation** — the selected policy is split into orthogonal controls: `task_move`, `social_move`, `repair_move`, and `realization`. The task is driven primarily by user intent, so affective adaptation cannot silently replace what the user actually asked for.
21
21
  6. **Steering** — the factored action, source policy, repair state, interaction quality, and canonical request are injected into the decode path so the control decision actually lands in the tokens.
22
22
  7. **Structural response audit** — the generated response is checked against inspectable control contracts such as question budget, required clarification, unsupported privacy promises, unknown-stance admissions, repair repetition, and low-verbosity overruns. This is not a factual truth checker.
@@ -93,7 +93,9 @@ class HeuristicAffect:
93
93
  low = text.lower().strip()
94
94
  hits = sum(1 for m in ESCALATION_MARKERS if m in low)
95
95
  softens = sum(1 for s in SOFTENERS if s in low)
96
- score = min(1.0, 0.25 + hits * 0.22 - softens * 0.12)
96
+ # Arousal is a [0, 1] scale: a softening-only turn must floor at 0, not
97
+ # go negative and hand the policy state an impossible value.
98
+ score = max(0.0, min(1.0, 0.25 + hits * 0.22 - softens * 0.12))
97
99
  if "?" in low or "!" in low:
98
100
  score = min(1.0, score + 0.1)
99
101
  return round(score, 3)
@@ -110,11 +112,13 @@ class HeuristicAffect:
110
112
 
111
113
  def _escalating(self, user_turns: list[str]) -> bool:
112
114
  """Three or more consecutive turns opening with a wh-word."""
113
- if len(user_turns) < 3:
115
+ # Blank turns are not wh-word turns. Counting them let three turns in
116
+ # which only one or two actually opened with a wh-word read as a
117
+ # three-turn interrogation run. Same filter as ``_demand_pattern``.
118
+ recent = [t for t in user_turns[-3:] if t.strip()]
119
+ if len(recent) < 3:
114
120
  return False
115
- return all(
116
- self._first_word(t) in QUESTION_OPENERS for t in user_turns[-3:] if t.strip()
117
- )
121
+ return all(self._first_word(t) in QUESTION_OPENERS for t in recent)
118
122
 
119
123
  def _first_word(self, text: str) -> str:
120
124
  """First token with punctuation stripped. 'Neden?' must match 'neden',
@@ -9,7 +9,7 @@ import subprocess
9
9
  import threading
10
10
  from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
- from typing import Iterable
12
+ from typing import Callable, Iterable
13
13
 
14
14
  from .auditor import audit_from_generation_metadata, regeneration_instruction
15
15
  from .policies import Selector
@@ -198,6 +198,17 @@ def stable_case_seed(global_seed: int, case_id: str) -> int:
198
198
  return int.from_bytes(digest[:4], "big", signed=False)
199
199
 
200
200
 
201
+ # The experiment's own clock. Reproducibility is a property of the artifact, so
202
+ # the wall clock must never leak into a generated record: the default is a
203
+ # fixed epoch and callers who need a real timestamp inject their own clock.
204
+ DEFAULT_EXPERIMENT_CLOCK_EPOCH = 0.0
205
+
206
+
207
+ def _fixed_experiment_clock() -> float:
208
+ """Default experiment clock: a constant, so runs differ only by content."""
209
+ return DEFAULT_EXPERIMENT_CLOCK_EPOCH
210
+
211
+
201
212
  def file_sha256(path: str | Path) -> str:
202
213
  digest = hashlib.sha256()
203
214
  with open(path, "rb") as fh:
@@ -654,7 +665,18 @@ def generate_pairs(
654
665
  auditor_regenerations: int = 0,
655
666
  length_matched_arm: bool = False,
656
667
  length_profiles: tuple[str, ...] | None = None,
668
+ rng: random.Random | None = None,
669
+ clock: Callable[[], float] | None = None,
657
670
  ) -> list[dict]:
671
+ """Generate the same-model baseline/EQ pairs for one experiment run.
672
+
673
+ Generation is deterministic: the only randomness is the control-arm
674
+ shuffling, seeded from ``seed``, and it is quarantined behind the injectable
675
+ ``rng`` so a caller can supply the same generator without re-deriving it
676
+ from the seed. Any timestamp is quarantined behind ``clock``, whose default
677
+ is a fixed epoch, so the same experiment plus the same fixture serialises
678
+ to identical bytes.
679
+ """
658
680
  if auditor_regenerations not in {0, 1}:
659
681
  raise ValueError("auditor_regenerations must be 0 or 1.")
660
682
  profiles = tuple(length_profiles or ())
@@ -665,7 +687,8 @@ def generate_pairs(
665
687
  f"{sorted(LENGTH_CONTROL_PROFILES)}"
666
688
  )
667
689
 
668
- rng = random.Random(seed)
690
+ rng = rng if rng is not None else random.Random(seed)
691
+ clock = clock if clock is not None else _fixed_experiment_clock
669
692
  output: list[dict] = []
670
693
  seen: set[str] = set()
671
694
 
@@ -769,6 +792,7 @@ def generate_pairs(
769
792
  "model_id": model.model_id,
770
793
  "temperature": model.temperature,
771
794
  "pair_seed": pair_seed,
795
+ "generated_at_epoch": round(float(clock()), 6),
772
796
  "order": ["baseline", "eq", *controls],
773
797
  "same_model_both_arms": True,
774
798
  "annotations_used": bool(annotated),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "eq-layer"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Inspectable intent, affect, repair, uncertainty, and factored-action control for LLM responses"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes