awpredict 0.1.2__tar.gz → 0.1.3__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.
- {awpredict-0.1.2 → awpredict-0.1.3}/PKG-INFO +1 -1
- {awpredict-0.1.2 → awpredict-0.1.3}/README.md +7 -1
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/__init__.py +4 -1
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/_doctor.py +9 -1
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/cli.py +2 -1
- awpredict-0.1.3/awpredict/core/concept.py +357 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/PKG-INFO +1 -1
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/SOURCES.txt +2 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/pyproject.toml +1 -1
- awpredict-0.1.3/tests/test_concept.py +125 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/LICENSE +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/adapters/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/adapters/code_world.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/adapters/obs_adapter.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/contracts.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/core/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/core/lewm.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/core/mlp.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/explorer/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/explorer/fs_cartographer.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/explorer/histogram_encoder.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/memory/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/memory/landmark.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/training/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/training/online.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/dependency_links.txt +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/entry_points.txt +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/requires.txt +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/top_level.txt +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/setup.cfg +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_fs_explorer_protocol.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_landmark_memory.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_lint_config_agrees.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_object_masking.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_online_lr_controller.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_problem_contracts.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_structured_adapter.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_value_head.py +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**[Docs](https://aitherium.github.io/awpredict/)** · [Source](https://github.com/Aitherium/awpredict) · `pip install git+https://github.com/Aitherium/awpredict.git` · [The Aither World](https://aitherium.github.io/)
|
|
6
6
|
|
|
7
|
-
> **The Aither World** is an operating system for agents — a Linux you can hand to one, the runtimes it works in, and the tools it works with. [awnix](https://github.com/Aitherium/awnix) is the Linux underneath it; **awpredict** is one of its
|
|
7
|
+
> **The Aither World** is an operating system for agents — a Linux you can hand to one, the runtimes it works in, and the tools it works with. [awnix](https://github.com/Aitherium/awnix) is the Linux underneath it; **awpredict** is one of its 68 bricks — each installs on its own, runs offline, and needs no account.
|
|
8
8
|
>
|
|
9
9
|
> **Start here:** Wrap an environment you already have and ask it what happens next.
|
|
10
10
|
|
|
@@ -136,6 +136,7 @@ Each installs on its own, works offline, and needs no account.
|
|
|
136
136
|
| [awtoll](https://github.com/Aitherium/awtoll) | that your tooling is saving you context | the measured token cost of each tool call, and what the alternative cost |
|
|
137
137
|
| [awseal](https://github.com/Aitherium/awseal) | that the artifact came from who you think | an Ed25519 seal — the key that verifies is not the key that forges |
|
|
138
138
|
| [awshare](https://github.com/Aitherium/awshare) | that the download is intact | content-addressed bundles, verified on fetch |
|
|
139
|
+
| [awsuite](https://github.com/Aitherium/awsuite) | that an agent holding your mailbox will not send on its own | every send, draft, upload and create returns a dry-run until confirm is true |
|
|
139
140
|
| [awnest](https://github.com/Aitherium/awnest) | that there is a person on the other end | a verdict with evidence, where "we could not tell" is not "yes" |
|
|
140
141
|
| [awrena](https://github.com/Aitherium/awrena) | a leaderboard someone can edit, and votes nobody counted | a scored duel with both answers kept, and a result bound to them |
|
|
141
142
|
| [awnboard](https://github.com/Aitherium/awnboard) | a share link anyone who sees it can use | an invitation addressed to one person, for one gate, revocable |
|
|
@@ -145,6 +146,7 @@ Each installs on its own, works offline, and needs no account.
|
|
|
145
146
|
| [awrelay](https://github.com/Aitherium/awrelay) | a SaaS in the middle of your agents | findings, alerts and coordination over your own transport |
|
|
146
147
|
| [awask](https://github.com/Aitherium/awask) | that anyone read the paragraph where you asked | the ask itself, with a button that steers the session that raised it |
|
|
147
148
|
| [awmail](https://github.com/Aitherium/awmail) | a mailbox somebody else can read | mail your agents send and receive over your own server |
|
|
149
|
+
| [awdiscord](https://github.com/Aitherium/awdiscord) | a self-bot on someone's account, or a bot that acts without asking | slash commands, rate limits, and approval buttons only your operators can press |
|
|
148
150
|
| [awswarm](https://github.com/Aitherium/awswarm) | that a model either fits your GPU or it doesn't run at all | a placement plan and an acquisition-probability estimate before you spend on a run |
|
|
149
151
|
| [awfind](https://github.com/Aitherium/awfind) | one vendor's idea of the web | results from whichever providers you configured |
|
|
150
152
|
| [awbrowse](https://github.com/Aitherium/awbrowse) | that the page said what you were told | the render, the DOM and the requests it made |
|
|
@@ -201,6 +203,7 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
|
|
|
201
203
|
| [awtoll](https://github.com/Aitherium/awtoll) | What every tool call costs you in context, measured from your own transcripts | [docs](https://aitherium.github.io/awtoll/) |
|
|
202
204
|
| [awseal](https://github.com/Aitherium/awseal) | Sign an artifact so a stranger can verify it | [docs](https://aitherium.github.io/awseal/) |
|
|
203
205
|
| [awshare](https://github.com/Aitherium/awshare) | Publish an artifact and fetch it back verified | [docs](https://aitherium.github.io/awshare/) |
|
|
206
|
+
| [awsuite](https://github.com/Aitherium/awsuite) | Your Google Workspace as agent tools, and no write happens without a yes | — |
|
|
204
207
|
| [awdit](https://github.com/Aitherium/awdit) | An append-only audit trail whose gaps are DETECTABLE | [docs](https://aitherium.github.io/awdit/) |
|
|
205
208
|
| [awbac](https://github.com/Aitherium/awbac) | Role-based access control that fails closed and explains itself | [docs](https://aitherium.github.io/awbac/) |
|
|
206
209
|
| [awiam](https://github.com/Aitherium/awiam) | Who is this caller? A directory and session store that fails honestly | [docs](https://aitherium.github.io/awiam/) |
|
|
@@ -214,6 +217,7 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
|
|
|
214
217
|
| [awrelay](https://github.com/Aitherium/awrelay) | Portable agent messaging — findings, alerts, coordination | [docs](https://aitherium.github.io/awrelay/) |
|
|
215
218
|
| [awask](https://github.com/Aitherium/awask) | Your agent asks you a question — and acts on your answer | [docs](https://aitherium.github.io/awask/) |
|
|
216
219
|
| [awmail](https://github.com/Aitherium/awmail) | Give an agent an email address — send, and actually receive | [docs](https://aitherium.github.io/awmail/) |
|
|
220
|
+
| [awdiscord](https://github.com/Aitherium/awdiscord) | Put any agent on Discord as a real bot, with an operator in the loop | — |
|
|
217
221
|
| [awnet](https://github.com/Aitherium/awnet) | The agentic web — agents host a mesh, and agents join one | [docs](https://aitherium.github.io/awnet/) |
|
|
218
222
|
| [awswarm](https://github.com/Aitherium/awswarm) | Run one model too big for any single GPU across a pool of small ones | — |
|
|
219
223
|
| [awfind](https://github.com/Aitherium/awfind) | A portable search client — query, results, ranking | [docs](https://aitherium.github.io/awfind/) |
|
|
@@ -253,6 +257,8 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
|
|
|
253
257
|
| [awsettings](https://github.com/Aitherium/awsettings) | Your agent's permissions and config, following you to the next machine | [docs](https://aitherium.github.io/awsettings/) |
|
|
254
258
|
| [awavatar](https://github.com/Aitherium/awavatar) | One character spec in, a rigged, animated, multi-style avatar pack out | [docs](https://aitherium.github.io/awavatar/) |
|
|
255
259
|
|
|
260
|
+
**Built on** [REA (Reverse Engineer Anything)](https://github.com/morluto/rea) · [llama.cpp](https://github.com/ggml-org/llama.cpp) · [vLLM](https://github.com/vllm-project/vllm) · [ComfyUI](https://github.com/comfyanonymous/ComfyUI) · [CentOS Stream](https://www.centos.org/centos-stream/) · [Podman](https://github.com/containers/podman) · [Docker](https://github.com/moby/moby) · [LanceDB](https://github.com/lancedb/lancedb) · [WireGuard](https://www.wireguard.com/) · [FFmpeg](https://ffmpeg.org/) · [Blender + Rigify](https://www.blender.org/) · [headroom](https://github.com/headroomlabs-ai/headroom) · [SANA](https://github.com/NVlabs/Sana) · [Hunyuan3D](https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1) · [repowise](https://github.com/repowise-dev/repowise) · [Playwright](https://github.com/microsoft/playwright) · [Chromium](https://www.chromium.org/) · [Next.js](https://github.com/vercel/next.js) · [React](https://github.com/facebook/react).
|
|
261
|
+
|
|
256
262
|
<div id="aither-constellation" data-self="awpredict"></div>
|
|
257
263
|
<script src="aither-constellation.js"></script>
|
|
258
264
|
|
|
@@ -11,6 +11,9 @@ One package, two engines, N environment adapters:
|
|
|
11
11
|
is the same engine the solving agent runs, not a cut-down demo of it —
|
|
12
12
|
the goal of this package is to give you everything needed to bootstrap
|
|
13
13
|
your own world model, not a subset of it.
|
|
14
|
+
* ``awpredict.core.concept.ConceptWorldModel`` — next-concept JEPA over
|
|
15
|
+
embedding sequences (product/residual VQ concept vocabulary + SIGReg);
|
|
16
|
+
``plan`` ranks candidate continuations, e.g. parallel drafts.
|
|
14
17
|
* ``awpredict.core.mlp.MLPWorldModel`` — an embedding-MLP transition
|
|
15
18
|
model (tabular → hybrid → neural).
|
|
16
19
|
|
|
@@ -35,7 +38,7 @@ from awpredict.contracts import (
|
|
|
35
38
|
conforms,
|
|
36
39
|
)
|
|
37
40
|
|
|
38
|
-
__version__ = "0.1.
|
|
41
|
+
__version__ = "0.1.3"
|
|
39
42
|
|
|
40
43
|
__all__ = [
|
|
41
44
|
"Attempt",
|
|
@@ -23,7 +23,15 @@ import sys
|
|
|
23
23
|
#: package cannot read the registry, and a doctor that guessed at the family
|
|
24
24
|
#: would go stale in silence. Regenerate to update.
|
|
25
25
|
SELF = 'awpredict'
|
|
26
|
-
FAMILY = [
|
|
26
|
+
FAMILY = [
|
|
27
|
+
'awask', 'awavatar', 'awbac', 'awbrain', 'awbrowse', 'awcams', 'awclassify', 'awdecide',
|
|
28
|
+
'awdeck', 'awdelphi', 'awdit', 'awembed', 'awevolve', 'awfind', 'awflow', 'awfocus', 'awgit',
|
|
29
|
+
'awgraph', 'awgym', 'awiam', 'awkno', 'awm', 'awmail', 'awmine', 'awnboard', 'awnest', 'awnet',
|
|
30
|
+
'awplay', 'awpool', 'awprism', 'awprove', 'awreason', 'awrecover', 'awrecurse', 'awrelay',
|
|
31
|
+
'awrena', 'awrepl', 'awreport', 'awresearch', 'awrise', 'awrouter', 'awrtifact', 'awrun',
|
|
32
|
+
'awscope', 'awscreen', 'awseal', 'awsettings', 'awshare', 'awsprite', 'awstorage', 'awsuite',
|
|
33
|
+
'awswarm', 'awtax', 'awtoll', 'awtunnel', 'awvision', 'awvoice', 'awwall', 'gawbbonet',
|
|
34
|
+
]
|
|
27
35
|
PAIRS_WITH = ['awm']
|
|
28
36
|
|
|
29
37
|
#: This brick's OWN config, read out of its source at generation time.
|
|
@@ -28,7 +28,8 @@ def cmd_engines(args) -> int:
|
|
|
28
28
|
missing' sends someone to install what they already have.
|
|
29
29
|
"""
|
|
30
30
|
rows = []
|
|
31
|
-
for name, mod in (("lewm", "awpredict.core.lewm"), ("mlp", "awpredict.core.mlp")
|
|
31
|
+
for name, mod in (("lewm", "awpredict.core.lewm"), ("mlp", "awpredict.core.mlp"),
|
|
32
|
+
("concept", "awpredict.core.concept")):
|
|
32
33
|
try:
|
|
33
34
|
m = importlib.import_module(mod)
|
|
34
35
|
except Exception as exc:
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""concept: a next-concept JEPA world model over embedding sequences.
|
|
2
|
+
|
|
3
|
+
Three published ideas, one engine:
|
|
4
|
+
|
|
5
|
+
* Next Concept Prediction (ConceptLM, arXiv:2602.08984). Mean-pool every
|
|
6
|
+
``k`` consecutive embeddings into a concept, quantize concepts with a
|
|
7
|
+
product codebook (S segments x N codes, SimVQ MLP over the codes so they
|
|
8
|
+
do not collapse), and predict the NEXT concept as the current one plus a softmax-weighted sum
|
|
9
|
+
of codes per segment. Loss = MSE(predicted, true next concept) + VQ loss.
|
|
10
|
+
* Coarse-to-fine codes (Next-Concept Modeling, HazyResearch ncm). The
|
|
11
|
+
quantizer is RESIDUAL across ``levels``: level 0 is the coarse concept
|
|
12
|
+
(a topic-sized id), each later level quantizes what the previous left.
|
|
13
|
+
``concept_ids`` returns the whole hierarchy, coarse first.
|
|
14
|
+
* SIGReg (LeWorldModel / LeJEPA). The prediction target is the encoder's
|
|
15
|
+
own output with NO stop-gradient and NO EMA; the Gaussian regularizer on
|
|
16
|
+
the concept latents is what prevents collapse, exactly as in lewm.py.
|
|
17
|
+
|
|
18
|
+
Observations are float sequences ``[T, d_in]`` (token hidden states from any
|
|
19
|
+
frozen LM, or one embedding per agent step). The engine never fetches
|
|
20
|
+
embeddings itself -- the caller supplies them -- so it stays dependency-light.
|
|
21
|
+
|
|
22
|
+
``plan(context, candidates)`` ranks candidate continuations (for example K
|
|
23
|
+
parallel drafts from a diffusion LM) by how close each one's concept lands to
|
|
24
|
+
the predicted next concept: the world model chooses, the fast generator
|
|
25
|
+
proposes.
|
|
26
|
+
|
|
27
|
+
Torch is optional at import time. Without it ``ok`` is False and every method
|
|
28
|
+
returns None / [] -- it never raises into a caller.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
import os
|
|
34
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import torch
|
|
40
|
+
import torch.nn as nn
|
|
41
|
+
import torch.nn.functional as F
|
|
42
|
+
_TORCH_OK = True
|
|
43
|
+
except Exception: # pragma: no cover - torch is an optional dependency here
|
|
44
|
+
torch = None # type: ignore[assignment]
|
|
45
|
+
nn = None # type: ignore[assignment]
|
|
46
|
+
F = None # type: ignore[assignment]
|
|
47
|
+
_TORCH_OK = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def enabled() -> bool:
|
|
51
|
+
"""Usable here: torch imports. No env gate -- nothing consumes it by default."""
|
|
52
|
+
return _TORCH_OK
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sigreg(z: "torch.Tensor", dirs: "torch.Tensor", t: "torch.Tensor") -> "torch.Tensor":
|
|
56
|
+
"""Epps-Pulley SIGReg over a batch of latents z [B, D], tested against N(0, I).
|
|
57
|
+
|
|
58
|
+
Unlike lewm.py the latents are NOT standardized first: a next-latent MSE
|
|
59
|
+
with no stop-gradient can otherwise win by shrinking the whole latent scale
|
|
60
|
+
(measured: in-order and shuffled surprise both fell to ~0.07). Testing the
|
|
61
|
+
raw projections pins mean 0 AND unit scale, as LeJEPA does."""
|
|
62
|
+
if z.shape[0] < 4:
|
|
63
|
+
return z.new_tensor(0.0)
|
|
64
|
+
proj = z @ dirs
|
|
65
|
+
arg = t[:, None, None] * proj[None, :, :]
|
|
66
|
+
# Complex ECF vs the N(0,1) characteristic function exp(-t^2/2). The
|
|
67
|
+
# modulus-only form is translation-invariant, so a collapse to ONE point
|
|
68
|
+
# scores |ecf|=1 everywhere with zero gradient -- measured stuck at 0.65.
|
|
69
|
+
re = arg.cos().mean(dim=1) - (-(t ** 2) / 2).exp()[:, None]
|
|
70
|
+
im = arg.sin().mean(dim=1)
|
|
71
|
+
return (re ** 2 + im ** 2).mean()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if _TORCH_OK:
|
|
75
|
+
|
|
76
|
+
class _ProductCodebook(nn.Module):
|
|
77
|
+
"""S independent codebooks of N codes; codes pass through a SimVQ MLP."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, dim: int, segments: int, codes: int):
|
|
80
|
+
super().__init__()
|
|
81
|
+
self.S, self.N, self.seg = segments, codes, dim // segments
|
|
82
|
+
self.raw = nn.Parameter(torch.randn(segments, codes, self.seg) * 0.5)
|
|
83
|
+
self.mlp = nn.Sequential(nn.Linear(self.seg, self.seg), nn.ReLU(),
|
|
84
|
+
nn.Linear(self.seg, self.seg))
|
|
85
|
+
|
|
86
|
+
def codes(self) -> "torch.Tensor":
|
|
87
|
+
return self.mlp(self.raw) # [S, N, seg]
|
|
88
|
+
|
|
89
|
+
def quantize(self, c: "torch.Tensor"):
|
|
90
|
+
"""c [..., D] -> (quantized [..., D], ids [..., S])."""
|
|
91
|
+
book = self.codes()
|
|
92
|
+
cs = c.reshape(*c.shape[:-1], self.S, self.seg) # [..., S, seg]
|
|
93
|
+
d = ((cs.unsqueeze(-2) - book) ** 2).sum(-1) # [..., S, N]
|
|
94
|
+
ids = d.argmin(-1) # [..., S]
|
|
95
|
+
seg_idx = torch.arange(self.S, device=c.device).expand_as(ids)
|
|
96
|
+
q = book[seg_idx, ids] # [..., S, seg]
|
|
97
|
+
return q.reshape(c.shape), ids
|
|
98
|
+
|
|
99
|
+
def soft(self, logits: "torch.Tensor") -> "torch.Tensor":
|
|
100
|
+
"""logits [..., S, N] -> weighted code sum [..., D] (differentiable)."""
|
|
101
|
+
w = logits.softmax(-1)
|
|
102
|
+
q = torch.einsum("...sn,snd->...sd", w, self.codes())
|
|
103
|
+
return q.reshape(*q.shape[:-2], self.S * self.seg)
|
|
104
|
+
|
|
105
|
+
class _ConceptNet(nn.Module):
|
|
106
|
+
def __init__(self, d_in: int, dim: int, segments: int, codes: int,
|
|
107
|
+
levels: int, layers: int, heads: int, max_concepts: int):
|
|
108
|
+
super().__init__()
|
|
109
|
+
# LayerNorm without affine pins every concept to norm sqrt(dim): the
|
|
110
|
+
# next-latent MSE cannot win by shrinking the latent scale, and
|
|
111
|
+
# SIGReg then only has to rule out the remaining collapse (every
|
|
112
|
+
# concept the same direction). Without it: z std 0.02, sigreg stuck.
|
|
113
|
+
self.enc = nn.Sequential(nn.Linear(d_in, dim), nn.GELU(), nn.Linear(dim, dim),
|
|
114
|
+
nn.LayerNorm(dim, elementwise_affine=False))
|
|
115
|
+
self.pos = nn.Parameter(torch.zeros(max_concepts, dim))
|
|
116
|
+
block = nn.TransformerEncoderLayer(dim, heads, dim * 2, dropout=0.0,
|
|
117
|
+
batch_first=True, norm_first=True)
|
|
118
|
+
self.ctx = nn.TransformerEncoder(block, layers, enable_nested_tensor=False)
|
|
119
|
+
self.books = nn.ModuleList(_ProductCodebook(dim, segments, codes)
|
|
120
|
+
for _ in range(levels))
|
|
121
|
+
self.heads = nn.ModuleList(nn.Linear(dim, segments * codes) for _ in range(levels))
|
|
122
|
+
self.S, self.N = segments, codes
|
|
123
|
+
|
|
124
|
+
def concepts(self, x: "torch.Tensor", k: int) -> "torch.Tensor":
|
|
125
|
+
"""x [B, T, d_in] -> concept latents [B, T//k, dim] (mean-pool then encode)."""
|
|
126
|
+
b, t, d = x.shape
|
|
127
|
+
m = t // k
|
|
128
|
+
pooled = x[:, : m * k].reshape(b, m, k, d).mean(2)
|
|
129
|
+
return self.enc(pooled)
|
|
130
|
+
|
|
131
|
+
def predict(self, c: "torch.Tensor") -> "torch.Tensor":
|
|
132
|
+
"""c [B, M, dim] -> predicted NEXT concept at every position [B, M, dim]."""
|
|
133
|
+
m = c.shape[1]
|
|
134
|
+
mask = torch.triu(torch.full((m, m), float("-inf"), device=c.device), 1)
|
|
135
|
+
h = self.ctx(c + self.pos[:m], mask=mask)
|
|
136
|
+
# RESIDUAL: next = current + a soft code mix. Predicting the absolute
|
|
137
|
+
# next concept from codes regressed to the mean: on real text plan@1
|
|
138
|
+
# was 8% (chance 10%) while "pick the candidate nearest the last
|
|
139
|
+
# concept" scored 52%. As a delta it is 82% (n=120, 2026-10-19).
|
|
140
|
+
out = c
|
|
141
|
+
for head, book in zip(self.heads, self.books):
|
|
142
|
+
logits = head(h).reshape(*h.shape[:-1], self.S, self.N)
|
|
143
|
+
out = out + book.soft(logits)
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
def residual_quantize(self, c: "torch.Tensor"):
|
|
147
|
+
"""Coarse-to-fine residual VQ: returns (sum of levels, [ids per level], vq_loss)."""
|
|
148
|
+
resid, total, ids, loss = c, torch.zeros_like(c), [], c.new_tensor(0.0)
|
|
149
|
+
for book in self.books:
|
|
150
|
+
q, i = book.quantize(resid.detach())
|
|
151
|
+
# codebook-only loss: ConceptLM confines L_VQ to the quantizer so
|
|
152
|
+
# the codes track the latents, never the reverse (a commitment
|
|
153
|
+
# term pulled the encoder onto a few codes and collapsed it).
|
|
154
|
+
loss = loss + F.mse_loss(q, resid.detach())
|
|
155
|
+
total = total + q
|
|
156
|
+
ids.append(i)
|
|
157
|
+
resid = resid - q.detach()
|
|
158
|
+
return total, ids, loss
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ConceptWorldModel:
|
|
162
|
+
"""Next-concept JEPA engine. See the module docstring for the recipe."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, d_in: int, dim: int = 128, k: int = 4, segments: int = 8,
|
|
165
|
+
codes: int = 64, levels: int = 2, layers: int = 2, heads: int = 4,
|
|
166
|
+
max_concepts: int = 256, sigreg_lambda: float = 1.0, lr: float = 1e-3,
|
|
167
|
+
buffer_size: int = 4096, seed: int = 0, device: Optional[str] = None):
|
|
168
|
+
self.ok = False
|
|
169
|
+
self.k, self.d_in, self.lam = k, d_in, sigreg_lambda
|
|
170
|
+
self.max_concepts = max_concepts
|
|
171
|
+
self._buf: List[Any] = []
|
|
172
|
+
self._buf_cap = buffer_size
|
|
173
|
+
self.steps = 0
|
|
174
|
+
if not _TORCH_OK:
|
|
175
|
+
logger.warning("concept: torch unavailable; engine disabled (ok=False)")
|
|
176
|
+
return
|
|
177
|
+
if dim % segments:
|
|
178
|
+
raise ValueError(f"dim {dim} must divide into {segments} segments")
|
|
179
|
+
g = torch.Generator().manual_seed(seed)
|
|
180
|
+
torch.manual_seed(seed)
|
|
181
|
+
self.device = torch.device(device or os.environ.get("AWPREDICT_DEVICE", "cpu"))
|
|
182
|
+
self.net = _ConceptNet(d_in, dim, segments, codes, levels, layers, heads,
|
|
183
|
+
max_concepts).to(self.device)
|
|
184
|
+
dirs = torch.randn(dim, 64, generator=g)
|
|
185
|
+
self._sig_dirs = (dirs / dirs.norm(dim=0, keepdim=True)).to(self.device)
|
|
186
|
+
self._sig_t = torch.linspace(0.2, 3.0, 17).to(self.device)
|
|
187
|
+
self.opt = torch.optim.AdamW(self.net.parameters(), lr=lr, weight_decay=1e-4)
|
|
188
|
+
self.cfg = dict(d_in=d_in, dim=dim, k=k, segments=segments, codes=codes,
|
|
189
|
+
levels=levels, layers=layers, heads=heads, max_concepts=max_concepts)
|
|
190
|
+
self.ok = True
|
|
191
|
+
|
|
192
|
+
# -- helpers -------------------------------------------------------------
|
|
193
|
+
def _seq(self, obs: Any) -> Optional["torch.Tensor"]:
|
|
194
|
+
"""obs [T, d_in] or [B, T, d_in] -> float tensor [B, T, d_in] on device."""
|
|
195
|
+
x = torch.as_tensor(obs, dtype=torch.float32, device=self.device)
|
|
196
|
+
if x.dim() == 2:
|
|
197
|
+
x = x.unsqueeze(0)
|
|
198
|
+
if x.dim() != 3 or x.shape[-1] != self.d_in or x.shape[1] < self.k:
|
|
199
|
+
return None
|
|
200
|
+
return x
|
|
201
|
+
|
|
202
|
+
# -- WorldModel protocol -------------------------------------------------
|
|
203
|
+
def observe(self, obs: Any, action: Any = None, next_obs: Any = None,
|
|
204
|
+
*args: Any, **kwargs: Any) -> bool:
|
|
205
|
+
"""Buffer one sequence. ``next_obs`` (if given) is appended to ``obs``."""
|
|
206
|
+
if not self.ok:
|
|
207
|
+
return False
|
|
208
|
+
seq = obs if next_obs is None else _concat(obs, next_obs)
|
|
209
|
+
x = self._seq(seq)
|
|
210
|
+
if x is None or x.shape[1] < 2 * self.k:
|
|
211
|
+
return False
|
|
212
|
+
self._buf.append(x[0].detach().cpu())
|
|
213
|
+
if len(self._buf) > self._buf_cap:
|
|
214
|
+
self._buf.pop(0)
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def encode(self, obs: Any, cond: Any = None) -> Optional["torch.Tensor"]:
|
|
218
|
+
"""Sequence -> concept latents [M, dim]."""
|
|
219
|
+
if not self.ok:
|
|
220
|
+
return None
|
|
221
|
+
x = self._seq(obs)
|
|
222
|
+
if x is None:
|
|
223
|
+
return None
|
|
224
|
+
with torch.no_grad():
|
|
225
|
+
return self.net.concepts(x, self.k)[0]
|
|
226
|
+
|
|
227
|
+
def predict(self, z: Any, action: Any = None, **kwargs: Any) -> Optional["torch.Tensor"]:
|
|
228
|
+
"""Concept latents [M, dim] -> predicted next concept [dim]."""
|
|
229
|
+
if not self.ok or z is None:
|
|
230
|
+
return None
|
|
231
|
+
with torch.no_grad():
|
|
232
|
+
c = torch.as_tensor(z, dtype=torch.float32, device=self.device)
|
|
233
|
+
return self.net.predict(c.unsqueeze(0)[:, -self.max_concepts:])[0, -1]
|
|
234
|
+
|
|
235
|
+
def surprise(self, obs: Any, action: Any = None, next_obs: Any = None,
|
|
236
|
+
*args: Any, **kwargs: Any) -> Optional[float]:
|
|
237
|
+
"""Mean squared next-concept prediction error over the sequence."""
|
|
238
|
+
if not self.ok:
|
|
239
|
+
return None
|
|
240
|
+
seq = obs if next_obs is None else _concat(obs, next_obs)
|
|
241
|
+
x = self._seq(seq)
|
|
242
|
+
if x is None or x.shape[1] < 2 * self.k:
|
|
243
|
+
return None
|
|
244
|
+
with torch.no_grad():
|
|
245
|
+
c = self.net.concepts(x, self.k)
|
|
246
|
+
pred = self.net.predict(c[:, :-1])
|
|
247
|
+
return float(((pred - c[:, 1:]) ** 2).sum(-1).mean().item())
|
|
248
|
+
|
|
249
|
+
def train_step(self, batch_size: int = 32, steps: int = 1,
|
|
250
|
+
*args: Any, **kwargs: Any) -> Optional[Dict[str, Any]]:
|
|
251
|
+
if not self.ok or not self._buf:
|
|
252
|
+
return None
|
|
253
|
+
out: Dict[str, Any] = {}
|
|
254
|
+
self.net.train()
|
|
255
|
+
for _ in range(max(1, steps)):
|
|
256
|
+
idx = torch.randint(len(self._buf), (min(batch_size, len(self._buf)),))
|
|
257
|
+
t = min(self._buf[i].shape[0] for i in idx.tolist())
|
|
258
|
+
t = (min(t, self.max_concepts * self.k) // self.k) * self.k
|
|
259
|
+
x = torch.stack([self._buf[i][:t] for i in idx.tolist()]).to(self.device)
|
|
260
|
+
c = self.net.concepts(x, self.k)
|
|
261
|
+
pred = self.net.predict(c[:, :-1])
|
|
262
|
+
l_ncp = F.mse_loss(pred, c[:, 1:])
|
|
263
|
+
_, _, l_vq = self.net.residual_quantize(c)
|
|
264
|
+
l_sig = _sigreg(c.reshape(-1, c.shape[-1]), self._sig_dirs, self._sig_t)
|
|
265
|
+
loss = l_ncp + l_vq + self.lam * l_sig
|
|
266
|
+
self.opt.zero_grad(set_to_none=True)
|
|
267
|
+
loss.backward()
|
|
268
|
+
torch.nn.utils.clip_grad_norm_(self.net.parameters(), 1.0)
|
|
269
|
+
self.opt.step()
|
|
270
|
+
self.steps += 1
|
|
271
|
+
out = {"loss": loss.item(), "ncp": l_ncp.item(), "vq": l_vq.item(),
|
|
272
|
+
"sigreg": l_sig.item(), "steps": self.steps}
|
|
273
|
+
self.net.eval()
|
|
274
|
+
return out
|
|
275
|
+
|
|
276
|
+
def plan(self, context: Any, candidates: Sequence[Any] = (), *args: Any,
|
|
277
|
+
**kwargs: Any) -> List[int]:
|
|
278
|
+
"""Rank candidate continuations of ``context`` best-first.
|
|
279
|
+
|
|
280
|
+
Each candidate is a ``[t, d_in]`` sequence (t >= k). Score = squared
|
|
281
|
+
distance between the predicted next concept and the candidate's first
|
|
282
|
+
concept. Use it to pick among K parallel drafts.
|
|
283
|
+
"""
|
|
284
|
+
if not self.ok or not candidates:
|
|
285
|
+
return []
|
|
286
|
+
z = self.encode(context)
|
|
287
|
+
if z is None:
|
|
288
|
+
return []
|
|
289
|
+
target = self.predict(z)
|
|
290
|
+
scores = []
|
|
291
|
+
for i, cand in enumerate(candidates):
|
|
292
|
+
zc = self.encode(cand)
|
|
293
|
+
d = float("inf") if zc is None else float(((zc[0] - target) ** 2).sum())
|
|
294
|
+
scores.append((d, i))
|
|
295
|
+
return [i for _, i in sorted(scores)]
|
|
296
|
+
|
|
297
|
+
# -- concept vocabulary --------------------------------------------------
|
|
298
|
+
def concept_ids(self, obs: Any) -> Optional[List[List[List[int]]]]:
|
|
299
|
+
"""Discrete coarse-to-fine codes per chunk: [M][level][segment]."""
|
|
300
|
+
if not self.ok:
|
|
301
|
+
return None
|
|
302
|
+
x = self._seq(obs)
|
|
303
|
+
if x is None:
|
|
304
|
+
return None
|
|
305
|
+
with torch.no_grad():
|
|
306
|
+
_, ids, _ = self.net.residual_quantize(self.net.concepts(x, self.k))
|
|
307
|
+
stacked = torch.stack(ids, dim=-2)[0] # [M, levels, S]
|
|
308
|
+
return stacked.tolist()
|
|
309
|
+
|
|
310
|
+
def codebook_usage(self, obs_batch: Sequence[Any]) -> Optional[float]:
|
|
311
|
+
"""Fraction of level-0 codes used across ``obs_batch`` (mean over segments)."""
|
|
312
|
+
if not self.ok:
|
|
313
|
+
return None
|
|
314
|
+
seen = [set() for _ in range(self.net.S)]
|
|
315
|
+
for obs in obs_batch:
|
|
316
|
+
ids = self.concept_ids(obs)
|
|
317
|
+
for chunk in ids or []:
|
|
318
|
+
for s, code in enumerate(chunk[0]):
|
|
319
|
+
seen[s].add(code)
|
|
320
|
+
return sum(len(s) for s in seen) / (self.net.S * self.net.N)
|
|
321
|
+
|
|
322
|
+
# -- persistence ---------------------------------------------------------
|
|
323
|
+
def save(self, path: str, *args: Any, **kwargs: Any) -> bool:
|
|
324
|
+
if not self.ok:
|
|
325
|
+
return False
|
|
326
|
+
tmp = f"{path}.tmp"
|
|
327
|
+
try:
|
|
328
|
+
torch.save({"cfg": self.cfg, "state": self.net.state_dict(),
|
|
329
|
+
"steps": self.steps}, tmp)
|
|
330
|
+
os.replace(tmp, path)
|
|
331
|
+
return True
|
|
332
|
+
except OSError as exc:
|
|
333
|
+
logger.warning("concept.save failed: %s", exc)
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
def load(self, path: str, *args: Any, **kwargs: Any) -> bool:
|
|
337
|
+
if not self.ok or not os.path.exists(path):
|
|
338
|
+
return False
|
|
339
|
+
try:
|
|
340
|
+
ck = torch.load(path, map_location=self.device, weights_only=True)
|
|
341
|
+
if ck.get("cfg") != self.cfg:
|
|
342
|
+
logger.warning("concept.load: config mismatch %s != %s", ck.get("cfg"), self.cfg)
|
|
343
|
+
return False
|
|
344
|
+
self.net.load_state_dict(ck["state"])
|
|
345
|
+
self.steps = int(ck.get("steps", 0))
|
|
346
|
+
self.net.eval()
|
|
347
|
+
return True
|
|
348
|
+
except (OSError, RuntimeError, KeyError) as exc:
|
|
349
|
+
logger.warning("concept.load failed: %s", exc)
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _concat(a: Any, b: Any) -> Any:
|
|
354
|
+
if _TORCH_OK:
|
|
355
|
+
return torch.cat([torch.as_tensor(a, dtype=torch.float32),
|
|
356
|
+
torch.as_tensor(b, dtype=torch.float32)], dim=-2)
|
|
357
|
+
return list(a) + list(b)
|
|
@@ -15,6 +15,7 @@ awpredict/adapters/__init__.py
|
|
|
15
15
|
awpredict/adapters/code_world.py
|
|
16
16
|
awpredict/adapters/obs_adapter.py
|
|
17
17
|
awpredict/core/__init__.py
|
|
18
|
+
awpredict/core/concept.py
|
|
18
19
|
awpredict/core/lewm.py
|
|
19
20
|
awpredict/core/mlp.py
|
|
20
21
|
awpredict/explorer/__init__.py
|
|
@@ -24,6 +25,7 @@ awpredict/memory/__init__.py
|
|
|
24
25
|
awpredict/memory/landmark.py
|
|
25
26
|
awpredict/training/__init__.py
|
|
26
27
|
awpredict/training/online.py
|
|
28
|
+
tests/test_concept.py
|
|
27
29
|
tests/test_fs_explorer_protocol.py
|
|
28
30
|
tests/test_landmark_memory.py
|
|
29
31
|
tests/test_lint_config_agrees.py
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
# `awm` is the shape that already cost this repo a scare with AitherEvents.py
|
|
14
14
|
# beside AitherEventSpine.py.
|
|
15
15
|
name = "awpredict"
|
|
16
|
-
version = "0.1.
|
|
16
|
+
version = "0.1.3"
|
|
17
17
|
description = "A small, dependency-light world-model package: LeWM JEPA + MLP engines, environment adapters"
|
|
18
18
|
license = {text = "MIT"}
|
|
19
19
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""ConceptWorldModel: learns concept dynamics, does not collapse, ranks drafts."""
|
|
2
|
+
import importlib.util
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from awpredict.contracts import WorldModel, conforms
|
|
6
|
+
|
|
7
|
+
_HAS_TORCH = importlib.util.find_spec("torch") is not None
|
|
8
|
+
needs_torch = pytest.mark.skipif(not _HAS_TORCH, reason="torch not installed")
|
|
9
|
+
|
|
10
|
+
D_IN, K, N_PROTO = 16, 4, 6
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _world(seed=0):
|
|
14
|
+
"""Concept prototypes whose order follows a fixed cycle: 0->1->...->5->0."""
|
|
15
|
+
import torch
|
|
16
|
+
g = torch.Generator().manual_seed(seed)
|
|
17
|
+
protos = torch.randn(N_PROTO, D_IN, generator=g) * 2.0
|
|
18
|
+
|
|
19
|
+
def seq(start, n_chunks, order=None, noise=0.3):
|
|
20
|
+
ids = order or [(start + i) % N_PROTO for i in range(n_chunks)]
|
|
21
|
+
rows = [protos[i] + noise * torch.randn(K, D_IN, generator=g) for i in ids]
|
|
22
|
+
return torch.cat(rows, 0), ids
|
|
23
|
+
|
|
24
|
+
return protos, seq
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="module")
|
|
28
|
+
def trained():
|
|
29
|
+
import torch
|
|
30
|
+
from awpredict.core.concept import ConceptWorldModel
|
|
31
|
+
torch.manual_seed(0)
|
|
32
|
+
torch.set_num_threads(min(4, torch.get_num_threads())) # 32 threads on a busy host: 25x slower
|
|
33
|
+
_, seq = _world()
|
|
34
|
+
m = ConceptWorldModel(d_in=D_IN, dim=64, k=K, segments=4, codes=16, levels=2,
|
|
35
|
+
layers=1, heads=4, max_concepts=32)
|
|
36
|
+
for i in range(256):
|
|
37
|
+
m.observe(seq(i % N_PROTO, 8)[0])
|
|
38
|
+
log = m.train_step(batch_size=16, steps=300)
|
|
39
|
+
return m, seq, log
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@needs_torch
|
|
43
|
+
def test_conforms_to_world_model_contract():
|
|
44
|
+
from awpredict.core.concept import ConceptWorldModel
|
|
45
|
+
m = ConceptWorldModel(d_in=D_IN)
|
|
46
|
+
assert conforms(m, WorldModel) == []
|
|
47
|
+
assert m.ok
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@needs_torch
|
|
51
|
+
def test_learns_the_concept_order(trained):
|
|
52
|
+
import torch
|
|
53
|
+
m, seq, _ = trained
|
|
54
|
+
in_order = sum(m.surprise(seq(s, 8)[0]) for s in range(N_PROTO))
|
|
55
|
+
g = torch.Generator().manual_seed(7)
|
|
56
|
+
shuffled = 0.0
|
|
57
|
+
for s in range(N_PROTO):
|
|
58
|
+
order = torch.randperm(N_PROTO, generator=g).tolist() + [s, (s + 3) % N_PROTO]
|
|
59
|
+
shuffled += m.surprise(seq(0, 8, order=order)[0])
|
|
60
|
+
assert shuffled > 3 * in_order, (in_order, shuffled)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@needs_torch
|
|
64
|
+
def test_no_collapse(trained):
|
|
65
|
+
m, seq, log = trained
|
|
66
|
+
z = m.encode(seq(0, 8)[0])
|
|
67
|
+
assert float(z.std(0).mean()) > 0.1
|
|
68
|
+
assert log["sigreg"] < 0.1
|
|
69
|
+
ids = [m.concept_ids(seq(s, 8)[0]) for s in range(N_PROTO)]
|
|
70
|
+
coarse = {tuple(chunk[0]) for row in ids for chunk in row}
|
|
71
|
+
assert len(coarse) >= N_PROTO - 1 # distinct concepts get distinct coarse codes
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@needs_torch
|
|
75
|
+
def test_plan_picks_the_true_continuation(trained):
|
|
76
|
+
m, seq, _ = trained
|
|
77
|
+
hits = 0
|
|
78
|
+
for s in range(N_PROTO):
|
|
79
|
+
ctx, _ = seq(s, 5)
|
|
80
|
+
nxt = (s + 5) % N_PROTO
|
|
81
|
+
cands = [seq(0, 1, order=[j])[0] for j in range(N_PROTO)]
|
|
82
|
+
hits += m.plan(ctx, cands)[0] == nxt
|
|
83
|
+
assert hits >= N_PROTO - 1
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@needs_torch
|
|
87
|
+
def test_save_load_roundtrip(trained, tmp_path):
|
|
88
|
+
import torch
|
|
89
|
+
from awpredict.core.concept import ConceptWorldModel
|
|
90
|
+
m, seq, _ = trained
|
|
91
|
+
p = str(tmp_path / "c.ckpt")
|
|
92
|
+
assert m.save(p)
|
|
93
|
+
m2 = ConceptWorldModel(**m.cfg)
|
|
94
|
+
assert m2.load(p)
|
|
95
|
+
x = seq(2, 6)[0]
|
|
96
|
+
assert torch.allclose(m.encode(x), m2.encode(x))
|
|
97
|
+
assert not m2.load(str(tmp_path / "missing.ckpt"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@needs_torch
|
|
101
|
+
def test_degrades_on_bad_input():
|
|
102
|
+
from awpredict.core.concept import ConceptWorldModel
|
|
103
|
+
m = ConceptWorldModel(d_in=D_IN)
|
|
104
|
+
assert m.encode([[0.0] * 3]) is None # wrong width
|
|
105
|
+
assert m.surprise([[0.0] * D_IN]) is None # shorter than two chunks
|
|
106
|
+
assert m.plan([[0.0] * D_IN] * 8, []) == []
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@needs_torch
|
|
110
|
+
def test_prediction_is_a_residual_on_the_current_concept():
|
|
111
|
+
# Predicting the absolute next concept regressed to the mean and ranked
|
|
112
|
+
# real-text drafts at chance; the delta form is what made plan() work.
|
|
113
|
+
import torch
|
|
114
|
+
from awpredict.core.concept import ConceptWorldModel
|
|
115
|
+
m = ConceptWorldModel(d_in=D_IN, dim=64, k=K, segments=4, codes=16, levels=2,
|
|
116
|
+
layers=1, heads=4, max_concepts=32)
|
|
117
|
+
for head in m.net.heads:
|
|
118
|
+
torch.nn.init.zeros_(head.weight)
|
|
119
|
+
torch.nn.init.zeros_(head.bias)
|
|
120
|
+
for book in m.net.books: # uniform logits -> mix = mean code
|
|
121
|
+
book.raw.data.zero_()
|
|
122
|
+
for p in book.mlp.parameters():
|
|
123
|
+
p.data.zero_()
|
|
124
|
+
c = torch.randn(1, 5, 64)
|
|
125
|
+
assert torch.allclose(m.net.predict(c), c, atol=1e-5)
|
|
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
|
|
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
|