interlens 0.1.0__py3-none-any.whl

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 (60) hide show
  1. interlens/__init__.py +85 -0
  2. interlens/context/__init__.py +23 -0
  3. interlens/context/context_policy.py +46 -0
  4. interlens/context/drop_oldest_policy.py +32 -0
  5. interlens/context/error_policy.py +22 -0
  6. interlens/context/sliding_window_policy.py +33 -0
  7. interlens/context/summarize_policy.py +47 -0
  8. interlens/context_item.py +23 -0
  9. interlens/conversation.py +393 -0
  10. interlens/execution_mode.py +23 -0
  11. interlens/factories.py +120 -0
  12. interlens/hooks/__init__.py +3 -0
  13. interlens/hooks/message_hook.py +49 -0
  14. interlens/interp/__init__.py +29 -0
  15. interlens/interp/activation_cache.py +132 -0
  16. interlens/interp/capture.py +85 -0
  17. interlens/interp/layers.py +23 -0
  18. interlens/interp/logprobs.py +30 -0
  19. interlens/interp/patching.py +50 -0
  20. interlens/interp/steering.py +62 -0
  21. interlens/loading/__init__.py +28 -0
  22. interlens/loading/load.py +69 -0
  23. interlens/loading/model_cache.py +38 -0
  24. interlens/loading/registry.py +106 -0
  25. interlens/message.py +24 -0
  26. interlens/participant/__init__.py +4 -0
  27. interlens/participant/config/__init__.py +10 -0
  28. interlens/participant/config/api_participant_config.py +46 -0
  29. interlens/participant/config/model_participant_config.py +122 -0
  30. interlens/participant/config/participant_config.py +79 -0
  31. interlens/participant/participant.py +105 -0
  32. interlens/participant/participants/api_client.py +54 -0
  33. interlens/participant/participants/api_participant.py +80 -0
  34. interlens/participant/participants/gemma.py +68 -0
  35. interlens/participant/participants/model_participant.py +404 -0
  36. interlens/participant/participants/qwen.py +4 -0
  37. interlens/participant/role.py +3 -0
  38. interlens/reasoning_visibility.py +24 -0
  39. interlens/runner/__init__.py +19 -0
  40. interlens/runner/analyzer_registry.py +23 -0
  41. interlens/runner/batched.py +49 -0
  42. interlens/runner/devices.py +15 -0
  43. interlens/runner/pool.py +160 -0
  44. interlens/runner/rollout.py +40 -0
  45. interlens/runner/spec.py +26 -0
  46. interlens/runner/worker_init.py +18 -0
  47. interlens/stop/__init__.py +16 -0
  48. interlens/stop/conditions.py +74 -0
  49. interlens/stop/stop_condition.py +41 -0
  50. interlens/template.py +87 -0
  51. interlens/tools/__init__.py +5 -0
  52. interlens/tools/registry.py +33 -0
  53. interlens/tools/tool.py +28 -0
  54. interlens/tools/tool_call.py +26 -0
  55. interlens/transcript.py +179 -0
  56. interlens/view.py +31 -0
  57. interlens-0.1.0.dist-info/METADATA +74 -0
  58. interlens-0.1.0.dist-info/RECORD +60 -0
  59. interlens-0.1.0.dist-info/WHEEL +4 -0
  60. interlens-0.1.0.dist-info/licenses/LICENSE +21 -0
interlens/__init__.py ADDED
@@ -0,0 +1,85 @@
1
+ from .message import Message
2
+ from .transcript import Transcript
3
+ from .context_item import ContextItem
4
+ from .conversation import Conversation
5
+ from .template import ConversationTemplate
6
+ from .reasoning_visibility import ReasoningVisibility
7
+ from .execution_mode import ExecutionMode
8
+ from .participant import Participant
9
+ from .participant.participants.model_participant import ModelParticipant
10
+ from .participant.participants.qwen import QwenModelParticipant
11
+ from .participant.participants.gemma import GemmaModelParticipant
12
+ from .participant.participants.api_participant import APIParticipant
13
+ from .participant.config import ParticipantConfig, ModelParticipantConfig, APIParticipantConfig
14
+ from .context import ContextPolicy, ErrorPolicy, DropOldestPolicy, SlidingWindowPolicy, SummarizePolicy
15
+ from .hooks import MessageHook, MessageHookResult, HookAction
16
+ from .stop import (
17
+ StopCondition,
18
+ AnyStopCondition,
19
+ TurnStopCondition,
20
+ TokenStopCondition,
21
+ ElapsedTimeStopCondition,
22
+ StopStringCondition,
23
+ )
24
+ from .interp import ActivationCache, CaptureSpec, SteeringSpec, Patch, token_logprobs, decoder_layers
25
+ from .tools import Tool, ToolCall, ToolResult, ToolRegistry, DEFAULT_REGISTRY
26
+ from .runner import (
27
+ available_devices, ConversationSpec, run_conversations, RunResult, RunReport, rollout,
28
+ register_analyzer, register_worker_init,
29
+ )
30
+ from .factories import conversation_from_models, conversation_from_ids, AutoModelParticipant, ModelLike
31
+
32
+ __all__ = [
33
+ "Message",
34
+ "Transcript",
35
+ "ContextItem",
36
+ "Conversation",
37
+ "ConversationTemplate",
38
+ "ReasoningVisibility",
39
+ "ExecutionMode",
40
+ "Participant",
41
+ "ModelParticipant",
42
+ "QwenModelParticipant",
43
+ "GemmaModelParticipant",
44
+ "APIParticipant",
45
+ "ParticipantConfig",
46
+ "ModelParticipantConfig",
47
+ "APIParticipantConfig",
48
+ "ContextPolicy",
49
+ "ErrorPolicy",
50
+ "DropOldestPolicy",
51
+ "SlidingWindowPolicy",
52
+ "SummarizePolicy",
53
+ "MessageHook",
54
+ "MessageHookResult",
55
+ "HookAction",
56
+ "StopCondition",
57
+ "AnyStopCondition",
58
+ "TurnStopCondition",
59
+ "TokenStopCondition",
60
+ "ElapsedTimeStopCondition",
61
+ "StopStringCondition",
62
+ "ActivationCache",
63
+ "CaptureSpec",
64
+ "SteeringSpec",
65
+ "Patch",
66
+ "token_logprobs",
67
+ "decoder_layers",
68
+ "Tool",
69
+ "ToolCall",
70
+ "ToolResult",
71
+ "ToolRegistry",
72
+ "DEFAULT_REGISTRY",
73
+ "available_devices",
74
+ "ConversationSpec",
75
+ "run_conversations",
76
+ "RunResult",
77
+ "RunReport",
78
+ "rollout",
79
+ "register_analyzer",
80
+ "register_worker_init",
81
+ "conversation_from_models",
82
+ "conversation_from_ids",
83
+ "AutoModelParticipant",
84
+ "ModelLike",
85
+ ]
@@ -0,0 +1,23 @@
1
+ from .context_policy import ContextPolicy
2
+ from .error_policy import ErrorPolicy
3
+ from .drop_oldest_policy import DropOldestPolicy
4
+ from .sliding_window_policy import SlidingWindowPolicy
5
+ from .summarize_policy import SummarizePolicy
6
+
7
+ # Registry for (de)serializing policies by ``kind`` (their class name), so templates round-trip.
8
+ _POLICIES = {c.__name__: c for c in (ErrorPolicy, DropOldestPolicy, SlidingWindowPolicy, SummarizePolicy)}
9
+
10
+
11
+ def context_policy_from_dict(data: dict) -> ContextPolicy:
12
+ params = {k: v for k, v in data.items() if k != "kind"}
13
+ return _POLICIES[data["kind"]](**params)
14
+
15
+
16
+ __all__ = [
17
+ "ContextPolicy",
18
+ "ErrorPolicy",
19
+ "DropOldestPolicy",
20
+ "SlidingWindowPolicy",
21
+ "SummarizePolicy",
22
+ "context_policy_from_dict",
23
+ ]
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from ..view import ViewSegment
6
+
7
+ # Origins that must never be trimmed by a context policy: the system block, the scenario/moderator seed, and
8
+ # private briefings all carry essential framing. Only ``turn`` segments (the growing dialogue middle) are
9
+ # eligible for trimming.
10
+ PRESERVED_ORIGINS = ("system", "moderator", "private_context")
11
+
12
+
13
+ class ContextPolicy(ABC):
14
+ """Decides how to fit a participant's view within its model's context window.
15
+
16
+ Crucially, ``fit`` runs on the **typed** ``ViewSegment`` list, *before* the family-specific ``finalize_view``
17
+ folds/merges anything. Operating pre-finalize means the policy can reliably preserve the system block and
18
+ moderator seed (their ``origin`` is still intact) and trim only ``turn`` segments, instead of trying to
19
+ reverse-engineer meaning out of already-folded text.
20
+ """
21
+
22
+ @abstractmethod
23
+ def fit(self, segments: list[ViewSegment], tokenizer, limit: int | None) -> list[ViewSegment]:
24
+ ...
25
+
26
+ def to_dict(self) -> dict:
27
+ """Serialize as ``{"kind": ..., **params}``. Subclasses with parameters extend the params; the default
28
+ covers parameterless policies."""
29
+ return {"kind": type(self).__name__}
30
+
31
+ @staticmethod
32
+ def _limit(tokenizer, limit: int | None) -> int:
33
+ """Resolve the effective token budget; falls back to the tokenizer's declared max length."""
34
+ if limit is not None:
35
+ return limit
36
+ return int(getattr(tokenizer, "model_max_length", 1_000_000_000) or 1_000_000_000)
37
+
38
+ @staticmethod
39
+ def _seg_tokens(segment: ViewSegment, tokenizer) -> int:
40
+ """Approximate token cost of a segment's content (ignores per-template wrapper tokens — close enough for
41
+ fitting decisions)."""
42
+ return len(tokenizer(segment.content, add_special_tokens=False).input_ids)
43
+
44
+ @classmethod
45
+ def _total_tokens(cls, segments: list[ViewSegment], tokenizer) -> int:
46
+ return sum(cls._seg_tokens(s, tokenizer) for s in segments)
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from .context_policy import ContextPolicy, PRESERVED_ORIGINS
4
+ from ..view import ViewSegment
5
+
6
+
7
+ class DropOldestPolicy(ContextPolicy):
8
+ """Drop the oldest ``turn`` segments (preserving system/moderator/private_context) until the view fits."""
9
+
10
+ def fit(self, segments: list[ViewSegment], tokenizer, limit: int | None) -> list[ViewSegment]:
11
+ budget = self._limit(tokenizer, limit)
12
+ if self._total_tokens(segments, tokenizer) <= budget:
13
+ return segments
14
+
15
+ # Indices of trimmable turns, oldest first.
16
+ turn_indices = [i for i, s in enumerate(segments) if s.origin == "turn"]
17
+ dropped: set[int] = set()
18
+ total = self._total_tokens(segments, tokenizer)
19
+ for i in turn_indices:
20
+ if total <= budget:
21
+ break
22
+ total -= self._seg_tokens(segments[i], tokenizer)
23
+ dropped.add(i)
24
+
25
+ kept = [s for i, s in enumerate(segments) if i not in dropped]
26
+ # If preserved framing alone still overflows, there's nothing left to trim — surface it rather than lie.
27
+ if self._total_tokens(kept, tokenizer) > budget:
28
+ raise ValueError(
29
+ f"cannot fit view within {budget} tokens even after dropping all turns "
30
+ f"(preserved {PRESERVED_ORIGINS} framing is too large)"
31
+ )
32
+ return kept
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from .context_policy import ContextPolicy
4
+ from ..view import ViewSegment
5
+
6
+
7
+ class ErrorPolicy(ContextPolicy):
8
+ """The safe default: raise if the view exceeds the context window rather than silently dropping content.
9
+
10
+ Silent truncation reads as "everything fit" when it didn't, which is exactly the kind of quiet data loss this
11
+ harness avoids. Callers who want trimming opt into ``DropOldestPolicy``/``SlidingWindowPolicy`` explicitly.
12
+ """
13
+
14
+ def fit(self, segments: list[ViewSegment], tokenizer, limit: int | None) -> list[ViewSegment]:
15
+ budget = self._limit(tokenizer, limit)
16
+ total = self._total_tokens(segments, tokenizer)
17
+ if total > budget:
18
+ raise ValueError(
19
+ f"view ~{total} tokens exceeds context limit {budget}; choose a DropOldest/SlidingWindow "
20
+ f"context policy or shorten the scenario"
21
+ )
22
+ return segments
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from .context_policy import ContextPolicy
4
+ from ..view import ViewSegment
5
+
6
+
7
+ class SlidingWindowPolicy(ContextPolicy):
8
+ """Keep the preserved framing plus the most recent ``keep_last`` turns; drop older turns.
9
+
10
+ ``keep_system=True`` (the default) preserves the system/moderator/private_context framing regardless of the
11
+ window; set it False to also let framing fall outside the window (rarely wanted). Unlike ``DropOldestPolicy``
12
+ this is a fixed-size window rather than a fit-to-budget trim, so it's predictable turn-to-turn.
13
+ """
14
+
15
+ def __init__(self, keep_last: int, keep_system: bool = True):
16
+ self.keep_last = keep_last
17
+ self.keep_system = keep_system
18
+
19
+ def to_dict(self) -> dict:
20
+ return {"kind": "SlidingWindowPolicy", "keep_last": self.keep_last, "keep_system": self.keep_system}
21
+
22
+ def fit(self, segments: list[ViewSegment], tokenizer, limit: int | None) -> list[ViewSegment]:
23
+ turn_indices = [i for i, s in enumerate(segments) if s.origin == "turn"]
24
+ keep_turns = set(turn_indices[-self.keep_last:]) if self.keep_last > 0 else set()
25
+
26
+ kept = []
27
+ for i, s in enumerate(segments):
28
+ if s.origin == "turn":
29
+ if i in keep_turns:
30
+ kept.append(s)
31
+ elif self.keep_system:
32
+ kept.append(s)
33
+ return kept
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from .context_policy import ContextPolicy, PRESERVED_ORIGINS
4
+ from ..view import ViewSegment
5
+
6
+
7
+ class SummarizePolicy(ContextPolicy):
8
+ """Compress older turns into a single summary segment instead of dropping them outright (the heaviest
9
+ policy).
10
+
11
+ Keeps the preserved framing (system / moderator / private_context) and the most recent ``keep_last`` turns
12
+ verbatim, and replaces the older middle turns with one summary segment produced by ``summarizer`` — a
13
+ callable ``list[str] -> str`` over the dropped turns' contents. With no summarizer it inserts a neutral
14
+ placeholder, so it degrades to a labelled drop rather than silently losing content.
15
+
16
+ ``summarizer`` is a live callable and so is not serialized; a loaded template gets ``summarizer=None`` and
17
+ the caller re-injects one if needed.
18
+ """
19
+
20
+ def __init__(self, keep_last: int = 4, summarizer=None):
21
+ self.keep_last = keep_last
22
+ self.summarizer = summarizer
23
+
24
+ def to_dict(self) -> dict:
25
+ return {"kind": "SummarizePolicy", "keep_last": self.keep_last}
26
+
27
+ def _summarize(self, texts: list[str]) -> str:
28
+ if self.summarizer is not None:
29
+ return self.summarizer(texts)
30
+ return f"[{len(texts)} earlier turns omitted]"
31
+
32
+ def fit(self, segments: list[ViewSegment], tokenizer, limit: int | None) -> list[ViewSegment]:
33
+ budget = self._limit(tokenizer, limit)
34
+ if self._total_tokens(segments, tokenizer) <= budget:
35
+ return segments
36
+
37
+ preserved = [s for s in segments if s.origin in PRESERVED_ORIGINS]
38
+ turns = [s for s in segments if s.origin == "turn"]
39
+ older = turns[: -self.keep_last] if self.keep_last > 0 else turns
40
+ recent = turns[-self.keep_last:] if self.keep_last > 0 else []
41
+ if not older:
42
+ return segments # nothing old enough to summarize; leave as-is
43
+
44
+ summary = ViewSegment(role="user", origin="moderator",
45
+ content="[Summary of earlier conversation]\n" + self._summarize([s.content for s in older]))
46
+ # Preserve original ordering: framing first, then the summary standing in for older turns, then recent.
47
+ return preserved + [summary] + recent
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .participant.role import Role
6
+
7
+
8
+ @dataclass
9
+ class ContextItem:
10
+ """A single item of *private* asymmetric knowledge given to one participant (a briefing, a document, a fact).
11
+
12
+ Deliberately distinct from ``Message``: a briefing is not a dialogue turn — it has no turn index and its
13
+ ``author`` is nominal — so overloading ``Message`` would give it misleading turn/authorship semantics.
14
+
15
+ Role semantics are pinned rather than role-swapped: a briefing renders by default as **user-provided
16
+ context** (``role_hint=USER``) — the participant reads it as information handed to it, not as its own prior
17
+ speech — with ``SYSTEM`` available for standing private instructions. Private context is injected only into
18
+ the owning participant's view and never enters the shared transcript.
19
+ """
20
+
21
+ content: str
22
+ role_hint: Role = "user"
23
+ author: str = "moderator"