weightspace 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.
- weightspace/__init__.py +45 -0
- weightspace/adapters.py +112 -0
- weightspace/agent.py +436 -0
- weightspace/cli.py +73 -0
- weightspace/llm/__init__.py +35 -0
- weightspace/llm/anthropic.py +125 -0
- weightspace/llm/base.py +71 -0
- weightspace/llm/mock.py +38 -0
- weightspace/llm/openai.py +108 -0
- weightspace/probes.py +60 -0
- weightspace/prompts.py +43 -0
- weightspace/render.py +151 -0
- weightspace/sandbox.py +68 -0
- weightspace/space.py +319 -0
- weightspace/tasks.py +57 -0
- weightspace-0.1.0.dist-info/METADATA +181 -0
- weightspace-0.1.0.dist-info/RECORD +19 -0
- weightspace-0.1.0.dist-info/WHEEL +4 -0
- weightspace-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .base import LLM, LLMResponse, ToolCall, normalize_thinking
|
|
4
|
+
from .mock import ScriptedLLM, call, reply
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_llm(spec: str | LLM, **kw) -> LLM:
|
|
8
|
+
"""`"openai:gpt-5"`, `"anthropic:claude-sonnet-4-5"`, or `"openrouter:<model>"`."""
|
|
9
|
+
if isinstance(spec, LLM):
|
|
10
|
+
return spec
|
|
11
|
+
provider, _, model = spec.partition(":")
|
|
12
|
+
if not model:
|
|
13
|
+
provider, model = "openai", provider
|
|
14
|
+
provider = provider.lower()
|
|
15
|
+
if provider == "anthropic":
|
|
16
|
+
from .anthropic import AnthropicChat
|
|
17
|
+
|
|
18
|
+
return AnthropicChat(model, **kw)
|
|
19
|
+
if provider == "openrouter":
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
from .openai import OpenAIChat
|
|
23
|
+
|
|
24
|
+
kw.setdefault("base_url", "https://openrouter.ai/api/v1")
|
|
25
|
+
kw.setdefault("api_key", os.environ.get("OPENROUTER_API_KEY"))
|
|
26
|
+
kw.setdefault("thinking_style", "openrouter")
|
|
27
|
+
return OpenAIChat(model, **kw)
|
|
28
|
+
if provider == "openai":
|
|
29
|
+
from .openai import OpenAIChat
|
|
30
|
+
|
|
31
|
+
return OpenAIChat(model, **kw)
|
|
32
|
+
raise ValueError(f"unknown provider {provider!r}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ["normalize_thinking", "LLM", "LLMResponse", "ToolCall", "ScriptedLLM", "call", "reply", "get_llm"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from .base import LLM, LLMResponse, ToolCall, normalize_thinking
|
|
6
|
+
|
|
7
|
+
THINKING_BUDGET = {"low": 2048, "medium": 8192, "high": 16384, "xhigh": 24576, "max": 32000}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def to_anthropic(messages: list[dict]) -> tuple[str, list[dict]]:
|
|
11
|
+
system, out = [], []
|
|
12
|
+
for m in messages:
|
|
13
|
+
role = m["role"]
|
|
14
|
+
if role == "system":
|
|
15
|
+
system.append(m["content"])
|
|
16
|
+
elif role == "user":
|
|
17
|
+
out.append({"role": "user", "content": [{"type": "text", "text": m["content"]}]})
|
|
18
|
+
elif role == "assistant":
|
|
19
|
+
if m.get("anthropic_content"):
|
|
20
|
+
blocks = m["anthropic_content"]
|
|
21
|
+
else:
|
|
22
|
+
blocks = [{"type": "text", "text": m["content"]}] if m.get("content") else []
|
|
23
|
+
blocks += [
|
|
24
|
+
{"type": "tool_use", "id": t["id"], "name": t["name"], "input": t["arguments"]}
|
|
25
|
+
for t in m.get("tool_calls") or []
|
|
26
|
+
]
|
|
27
|
+
out.append({"role": "assistant", "content": blocks})
|
|
28
|
+
elif role == "tool":
|
|
29
|
+
block = {"type": "tool_result", "tool_use_id": m["tool_call_id"], "content": m["content"]}
|
|
30
|
+
if out and out[-1]["role"] == "user" and all(b["type"] == "tool_result" for b in out[-1]["content"]):
|
|
31
|
+
out[-1]["content"].append(block)
|
|
32
|
+
else:
|
|
33
|
+
out.append({"role": "user", "content": [block]})
|
|
34
|
+
merged: list[dict] = []
|
|
35
|
+
for m in out:
|
|
36
|
+
if merged and merged[-1]["role"] == m["role"] == "user":
|
|
37
|
+
merged[-1]["content"].extend(m["content"])
|
|
38
|
+
else:
|
|
39
|
+
merged.append(m)
|
|
40
|
+
return "\n\n".join(system), merged
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AnthropicChat(LLM):
|
|
44
|
+
"""Anthropic Messages API.
|
|
45
|
+
|
|
46
|
+
thinking_style: "adaptive" (thinking.type=adaptive + output_config.effort, newer models), "budget"
|
|
47
|
+
(thinking.type=enabled + budget_tokens, older models) or "auto" (adaptive, falling back to budget if the
|
|
48
|
+
model rejects it).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
model: str,
|
|
54
|
+
*,
|
|
55
|
+
api_key: str | None = None,
|
|
56
|
+
max_tokens: int = 16000,
|
|
57
|
+
client=None,
|
|
58
|
+
thinking=None,
|
|
59
|
+
thinking_style: str = "auto",
|
|
60
|
+
cache: bool = True,
|
|
61
|
+
**params,
|
|
62
|
+
):
|
|
63
|
+
if client is None:
|
|
64
|
+
from anthropic import Anthropic
|
|
65
|
+
|
|
66
|
+
client = Anthropic(api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"))
|
|
67
|
+
self.client = client
|
|
68
|
+
self.model = model
|
|
69
|
+
self.name = f"anthropic:{model}"
|
|
70
|
+
self.max_tokens = max_tokens
|
|
71
|
+
self.params = params
|
|
72
|
+
self.thinking = normalize_thinking(thinking)
|
|
73
|
+
if thinking_style not in ("auto", "adaptive", "budget"):
|
|
74
|
+
raise ValueError("thinking_style must be 'auto', 'adaptive' or 'budget'")
|
|
75
|
+
self.thinking_style = thinking_style
|
|
76
|
+
self.cache = cache
|
|
77
|
+
|
|
78
|
+
def _thinking_kwargs(self, style: str) -> tuple[dict, int]:
|
|
79
|
+
t = normalize_thinking(self.thinking)
|
|
80
|
+
if t is None:
|
|
81
|
+
return {}, self.max_tokens
|
|
82
|
+
if t == "off":
|
|
83
|
+
return {"thinking": {"type": "disabled"}}, self.max_tokens
|
|
84
|
+
if style == "budget":
|
|
85
|
+
budget = THINKING_BUDGET[t]
|
|
86
|
+
return {"thinking": {"type": "enabled", "budget_tokens": budget}}, max(self.max_tokens, budget + 4096)
|
|
87
|
+
return {"thinking": {"type": "adaptive"}, "output_config": {"effort": t}}, self.max_tokens
|
|
88
|
+
|
|
89
|
+
def complete(self, messages, tools=None):
|
|
90
|
+
system, msgs = to_anthropic(messages)
|
|
91
|
+
if self.cache and msgs and msgs[-1]["content"]:
|
|
92
|
+
# prompt caching: everything up to the newest block is reused on the next step
|
|
93
|
+
msgs[-1]["content"][-1] = {**msgs[-1]["content"][-1], "cache_control": {"type": "ephemeral"}}
|
|
94
|
+
kw = dict(self.params)
|
|
95
|
+
if system:
|
|
96
|
+
kw["system"] = system
|
|
97
|
+
if tools:
|
|
98
|
+
kw["tools"] = [{"name": t["name"], "description": t["description"], "input_schema": t["parameters"]} for t in tools]
|
|
99
|
+
style = "adaptive" if self.thinking_style == "auto" else self.thinking_style
|
|
100
|
+
tk, max_tokens = self._thinking_kwargs(style)
|
|
101
|
+
try:
|
|
102
|
+
r = self.client.messages.create(model=self.model, max_tokens=max_tokens, messages=msgs, **kw, **tk)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
if self.thinking_style != "auto" or "thinking" not in tk or "adaptive" not in str(e):
|
|
105
|
+
raise
|
|
106
|
+
self.thinking_style = "budget"
|
|
107
|
+
tk, max_tokens = self._thinking_kwargs("budget")
|
|
108
|
+
r = self.client.messages.create(model=self.model, max_tokens=max_tokens, messages=msgs, **kw, **tk)
|
|
109
|
+
if self.thinking_style == "auto":
|
|
110
|
+
self.thinking_style = style
|
|
111
|
+
text, calls, blocks = [], [], []
|
|
112
|
+
for b in r.content:
|
|
113
|
+
blocks.append(b.model_dump(exclude_none=True) if hasattr(b, "model_dump") else dict(b))
|
|
114
|
+
if b.type == "text":
|
|
115
|
+
text.append(b.text)
|
|
116
|
+
elif b.type == "tool_use":
|
|
117
|
+
calls.append(ToolCall(b.id, b.name, dict(b.input)))
|
|
118
|
+
usage = r.usage.model_dump() if getattr(r, "usage", None) is not None else {}
|
|
119
|
+
return LLMResponse(
|
|
120
|
+
text="\n".join(text) or None,
|
|
121
|
+
tool_calls=calls,
|
|
122
|
+
usage=usage,
|
|
123
|
+
raw=r,
|
|
124
|
+
extra={"anthropic_content": blocks},
|
|
125
|
+
)
|
weightspace/llm/base.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Provider-neutral chat interface.
|
|
2
|
+
|
|
3
|
+
Messages use an OpenAI-like neutral shape:
|
|
4
|
+
{"role": "system"|"user", "content": str}
|
|
5
|
+
{"role": "assistant", "content": str|None, "tool_calls": [{"id", "name", "arguments"}]}
|
|
6
|
+
{"role": "tool", "tool_call_id": str, "name": str, "content": str}
|
|
7
|
+
Tools are {"name", "description", "parameters": <JSON schema>}.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ToolCall:
|
|
18
|
+
id: str
|
|
19
|
+
name: str
|
|
20
|
+
arguments: dict
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict:
|
|
23
|
+
return {"id": self.id, "name": self.name, "arguments": self.arguments}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class LLMResponse:
|
|
28
|
+
text: str | None = None
|
|
29
|
+
tool_calls: list[ToolCall] = field(default_factory=list)
|
|
30
|
+
usage: dict = field(default_factory=dict)
|
|
31
|
+
raw: Any = None
|
|
32
|
+
extra: dict = field(default_factory=dict) # provider state to replay (e.g. thinking blocks)
|
|
33
|
+
|
|
34
|
+
def to_message(self) -> dict:
|
|
35
|
+
msg = {"role": "assistant", "content": self.text, "tool_calls": [t.to_dict() for t in self.tool_calls]}
|
|
36
|
+
msg.update(self.extra)
|
|
37
|
+
return msg
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
THINKING_LEVELS = ("off", "low", "medium", "high", "xhigh", "max")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_thinking(value) -> str | None:
|
|
44
|
+
"""None = provider default; False/"off" = no reasoning; True = "medium"; or "low"/"medium"/"high"."""
|
|
45
|
+
if value is None:
|
|
46
|
+
return None
|
|
47
|
+
if value is False:
|
|
48
|
+
return "off"
|
|
49
|
+
if value is True:
|
|
50
|
+
return "medium"
|
|
51
|
+
v = str(value).lower()
|
|
52
|
+
if v in ("none", "disabled", "false", "no"):
|
|
53
|
+
return "off"
|
|
54
|
+
if v in ("on", "true", "enabled"):
|
|
55
|
+
return "medium"
|
|
56
|
+
if v not in THINKING_LEVELS:
|
|
57
|
+
raise ValueError(f"thinking must be None, a bool, or one of {THINKING_LEVELS}")
|
|
58
|
+
return v
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LLM:
|
|
62
|
+
"""Implement `complete` to plug in any provider.
|
|
63
|
+
|
|
64
|
+
`thinking` is set by `Session(thinking=...)`; backends map it to their own reasoning switch.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
name = "llm"
|
|
68
|
+
thinking: str | None = None
|
|
69
|
+
|
|
70
|
+
def complete(self, messages: list[dict], tools: list[dict] | None = None) -> LLMResponse:
|
|
71
|
+
raise NotImplementedError
|
weightspace/llm/mock.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
from .base import LLM, LLMResponse, ToolCall
|
|
7
|
+
|
|
8
|
+
_ids = itertools.count()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def call(name: str, /, **arguments) -> ToolCall:
|
|
12
|
+
return ToolCall(f"call_{next(_ids)}", name, arguments)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def reply(text: str | None = None, *calls: ToolCall) -> LLMResponse:
|
|
16
|
+
return LLMResponse(text=text, tool_calls=list(calls))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ScriptedLLM(LLM):
|
|
20
|
+
"""Replays a fixed script; each step is an LLMResponse or fn(messages, tools) -> LLMResponse.
|
|
21
|
+
|
|
22
|
+
Useful for tests and for replaying logged runs without paying for tokens.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name = "scripted"
|
|
26
|
+
|
|
27
|
+
def __init__(self, steps: list[LLMResponse | Callable[[list[dict], list[dict]], LLMResponse]]):
|
|
28
|
+
self.steps = list(steps)
|
|
29
|
+
self.seen: list[list[dict]] = []
|
|
30
|
+
self.thinking_seen: list[str | None] = []
|
|
31
|
+
|
|
32
|
+
def complete(self, messages, tools=None):
|
|
33
|
+
self.seen.append([dict(m) for m in messages])
|
|
34
|
+
self.thinking_seen.append(self.thinking)
|
|
35
|
+
if not self.steps:
|
|
36
|
+
return reply("(script exhausted)")
|
|
37
|
+
step = self.steps.pop(0)
|
|
38
|
+
return step(messages, tools or []) if callable(step) else step
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from .base import LLM, LLMResponse, ToolCall, normalize_thinking
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def to_openai_messages(messages: list[dict]) -> list[dict]:
|
|
10
|
+
out = []
|
|
11
|
+
for m in messages:
|
|
12
|
+
if m["role"] == "assistant":
|
|
13
|
+
msg = {"role": "assistant", "content": m.get("content")}
|
|
14
|
+
if m.get("tool_calls"):
|
|
15
|
+
msg["tool_calls"] = [
|
|
16
|
+
{
|
|
17
|
+
"id": t["id"],
|
|
18
|
+
"type": "function",
|
|
19
|
+
"function": {"name": t["name"], "arguments": json.dumps(t["arguments"])},
|
|
20
|
+
}
|
|
21
|
+
for t in m["tool_calls"]
|
|
22
|
+
]
|
|
23
|
+
out.append(msg)
|
|
24
|
+
elif m["role"] == "tool":
|
|
25
|
+
out.append({"role": "tool", "tool_call_id": m["tool_call_id"], "content": m["content"]})
|
|
26
|
+
else:
|
|
27
|
+
out.append({"role": m["role"], "content": m["content"]})
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_openai_tools(tools: list[dict]) -> list[dict]:
|
|
32
|
+
return [
|
|
33
|
+
{"type": "function", "function": {"name": t["name"], "description": t["description"], "parameters": t["parameters"]}}
|
|
34
|
+
for t in tools
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OpenAIChat(LLM):
|
|
39
|
+
"""Any OpenAI-compatible Chat Completions endpoint (OpenAI, OpenRouter, vLLM, Ollama...).
|
|
40
|
+
|
|
41
|
+
thinking_style says how `thinking` is sent:
|
|
42
|
+
"reasoning_effort" OpenAI reasoning models (off -> `off_effort`, e.g. "none" or "minimal")
|
|
43
|
+
"openrouter" OpenRouter's unified `reasoning` field
|
|
44
|
+
"chat_template" vLLM / SGLang hybrid models (Qwen3...): `chat_template_kwargs.enable_thinking`
|
|
45
|
+
None never send anything
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
model: str,
|
|
51
|
+
*,
|
|
52
|
+
api_key: str | None = None,
|
|
53
|
+
base_url: str | None = None,
|
|
54
|
+
client=None,
|
|
55
|
+
thinking=None,
|
|
56
|
+
thinking_style: str | None = "reasoning_effort",
|
|
57
|
+
off_effort: str = "none",
|
|
58
|
+
**params,
|
|
59
|
+
):
|
|
60
|
+
if client is None:
|
|
61
|
+
from openai import OpenAI
|
|
62
|
+
|
|
63
|
+
client = OpenAI(
|
|
64
|
+
api_key=api_key or os.environ.get("OPENAI_API_KEY"),
|
|
65
|
+
base_url=base_url or os.environ.get("OPENAI_BASE_URL"),
|
|
66
|
+
)
|
|
67
|
+
self.client = client
|
|
68
|
+
self.model = model
|
|
69
|
+
self.name = f"openai:{model}"
|
|
70
|
+
self.params = params
|
|
71
|
+
self.thinking = normalize_thinking(thinking)
|
|
72
|
+
self.thinking_style = thinking_style
|
|
73
|
+
self.off_effort = off_effort
|
|
74
|
+
|
|
75
|
+
def _thinking_params(self, kw: dict) -> None:
|
|
76
|
+
t, style = normalize_thinking(self.thinking), self.thinking_style
|
|
77
|
+
if t is None or style is None:
|
|
78
|
+
return
|
|
79
|
+
extra = dict(kw.get("extra_body") or {})
|
|
80
|
+
if style == "reasoning_effort":
|
|
81
|
+
kw["reasoning_effort"] = self.off_effort if t == "off" else t
|
|
82
|
+
elif style == "openrouter":
|
|
83
|
+
extra["reasoning"] = {"enabled": False} if t == "off" else {"effort": t}
|
|
84
|
+
elif style == "chat_template":
|
|
85
|
+
ctk = dict(extra.get("chat_template_kwargs") or {})
|
|
86
|
+
ctk["enable_thinking"] = t != "off"
|
|
87
|
+
extra["chat_template_kwargs"] = ctk
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(f"unknown thinking_style {style!r}")
|
|
90
|
+
if extra:
|
|
91
|
+
kw["extra_body"] = extra
|
|
92
|
+
|
|
93
|
+
def complete(self, messages, tools=None):
|
|
94
|
+
kw = dict(self.params)
|
|
95
|
+
self._thinking_params(kw)
|
|
96
|
+
if tools:
|
|
97
|
+
kw["tools"] = to_openai_tools(tools)
|
|
98
|
+
r = self.client.chat.completions.create(model=self.model, messages=to_openai_messages(messages), **kw)
|
|
99
|
+
msg = r.choices[0].message
|
|
100
|
+
calls = []
|
|
101
|
+
for t in msg.tool_calls or []:
|
|
102
|
+
try:
|
|
103
|
+
args = json.loads(t.function.arguments or "{}")
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
args = {"_raw": t.function.arguments}
|
|
106
|
+
calls.append(ToolCall(t.id, t.function.name, args))
|
|
107
|
+
usage = r.usage.model_dump() if getattr(r, "usage", None) is not None else {}
|
|
108
|
+
return LLMResponse(text=msg.content, tool_calls=calls, usage=usage, raw=r)
|
weightspace/probes.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Forward passes the LLM may spend, defined by the user."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Probe:
|
|
13
|
+
"""A user-defined way to run the network, exposed to the LLM as a tool.
|
|
14
|
+
|
|
15
|
+
`fn` is called with the LLM's arguments after the current weights are
|
|
16
|
+
synced into the model. It decides what the LLM gets to see (scores,
|
|
17
|
+
rollouts, attention maps, generated text...).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
fn: Callable[..., Any]
|
|
21
|
+
name: str = ""
|
|
22
|
+
description: str = ""
|
|
23
|
+
parameters: dict = field(default_factory=dict)
|
|
24
|
+
cost: int = 1
|
|
25
|
+
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
self.name = self.name or self.fn.__name__
|
|
28
|
+
self.description = self.description or inspect.getdoc(self.fn) or f"Run {self.name}."
|
|
29
|
+
if not self.parameters:
|
|
30
|
+
self.parameters = _schema_from_signature(self.fn)
|
|
31
|
+
|
|
32
|
+
def __call__(self, **kw):
|
|
33
|
+
return self.fn(**kw)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def probe(fn=None, *, name: str = "", description: str = "", parameters: dict | None = None, cost: int = 1):
|
|
37
|
+
"""Decorator: `@probe` or `@probe(cost=2, parameters={...})`."""
|
|
38
|
+
|
|
39
|
+
def wrap(f):
|
|
40
|
+
return Probe(f, name=name, description=description, parameters=parameters or {}, cost=cost)
|
|
41
|
+
|
|
42
|
+
return wrap(fn) if fn is not None else wrap
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_JSON_TYPES = {int: "integer", float: "number", str: "string", bool: "boolean", list: "array", dict: "object"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _schema_from_signature(fn) -> dict:
|
|
49
|
+
props, required = {}, []
|
|
50
|
+
for p in inspect.signature(fn).parameters.values():
|
|
51
|
+
if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD):
|
|
52
|
+
continue
|
|
53
|
+
ann = p.annotation
|
|
54
|
+
origin = getattr(ann, "__origin__", ann)
|
|
55
|
+
props[p.name] = {"type": _JSON_TYPES.get(origin, "string")} if ann is not p.empty else {}
|
|
56
|
+
if p.default is p.empty:
|
|
57
|
+
required.append(p.name)
|
|
58
|
+
else:
|
|
59
|
+
props[p.name]["default"] = p.default if isinstance(p.default, (int, float, str, bool, type(None))) else str(p.default)
|
|
60
|
+
return {"type": "object", "properties": props, "required": required}
|
weightspace/prompts.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
SYSTEM = """You are a weight-space programmer. You read and write the weights of a neural network directly,
|
|
2
|
+
by reasoning about what each part of the network computes or should compute.
|
|
3
|
+
|
|
4
|
+
Rules:
|
|
5
|
+
- No gradient descent, no training loops, no fitting weights to data (no least squares on activations,
|
|
6
|
+
no optimizers, no search over random perturbations). Derive every edit from your understanding of the
|
|
7
|
+
network.
|
|
8
|
+
- Do not run the network or any of its layers on inputs inside the Python sandbox, even on made-up inputs.
|
|
9
|
+
Static analysis of the weights (norms, SVD, products of weight matrices, comparisons between tensors)
|
|
10
|
+
is fine. The only way to see the network's behaviour is the forward_* tools, which spend a limited budget.
|
|
11
|
+
- Prefer small, targeted edits that fix the cause and leave the rest of the network intact.
|
|
12
|
+
- When you are done, call `submit` exactly once with your answer and a confidence between 0 and 1.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
MODES = {
|
|
16
|
+
"read": "Task type: READ. Work out the answer from the weights. You cannot change any weights.",
|
|
17
|
+
"fix": (
|
|
18
|
+
"Task type: FIX. Find the cause of the described failure in the weights, explain it, and fix only that "
|
|
19
|
+
"cause by editing weights. Use checkpoints so you can roll back an edit that does not help."
|
|
20
|
+
),
|
|
21
|
+
"write": (
|
|
22
|
+
"Task type: WRITE. Program the network's weights so that it implements the specification. Decide what "
|
|
23
|
+
"algorithm each layer should compute, then convert it into weights."
|
|
24
|
+
),
|
|
25
|
+
"improve": (
|
|
26
|
+
"Task type: IMPROVE. Make the network better at the stated goal by editing weights. Diagnose what limits "
|
|
27
|
+
"it first, then make targeted edits, and keep an edit only if the evidence says it helps."
|
|
28
|
+
),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
NO_THINKING = """
|
|
32
|
+
Thinking is off for this run: do not write out step-by-step reasoning. Go straight to tool calls and weight
|
|
33
|
+
edits, with at most one short sentence of explanation each.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
TEXT_MODE_EDIT = """Reply with your reasoning and then the edit, in ONE of these formats:
|
|
37
|
+
1. A ```python block. It runs once with `W` (writable dict of numpy arrays, edit in place or assign),
|
|
38
|
+
`R` (read-only originals) and `np` in scope.
|
|
39
|
+
2. One or more ```weights blocks containing `### <tensor name> [<shape>]` headers each followed by all of
|
|
40
|
+
that tensor's numbers, in the same layout as the dump you were given.
|
|
41
|
+
End with a line `ANSWER: <short summary>` and a line `CONFIDENCE: <0..1>`."""
|
|
42
|
+
|
|
43
|
+
TEXT_MODE_READ = "Reply with your reasoning, then a line `ANSWER: <answer>` and a line `CONFIDENCE: <0..1>`."
|
weightspace/render.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Turning tensors into text an LLM can read, and back."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fmt_num(x: float, precision: int = 3) -> str:
|
|
11
|
+
s = f"{x:.{precision}g}"
|
|
12
|
+
return "0" if s in ("0", "-0") else s
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fmt_array(arr: np.ndarray, precision: int = 3) -> str:
|
|
16
|
+
"""One line per row (last axis); leading axes are labelled."""
|
|
17
|
+
arr = np.asarray(arr)
|
|
18
|
+
if arr.ndim == 0:
|
|
19
|
+
return fmt_num(float(arr), precision)
|
|
20
|
+
if arr.ndim == 1:
|
|
21
|
+
return " ".join(fmt_num(float(v), precision) for v in arr)
|
|
22
|
+
flat = arr.reshape(-1, arr.shape[-1])
|
|
23
|
+
lead = list(np.ndindex(*arr.shape[:-1]))
|
|
24
|
+
rows = []
|
|
25
|
+
for idx, row in zip(lead, flat, strict=True):
|
|
26
|
+
label = ",".join(str(i) for i in idx)
|
|
27
|
+
rows.append(f"[{label}] " + " ".join(fmt_num(float(v), precision) for v in row))
|
|
28
|
+
return "\n".join(rows)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_SLICE_PART = re.compile(r"^\s*(-?\d+)?\s*(?::\s*(-?\d+)?\s*(?::\s*(-?\d+)?\s*)?)?$")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_index(index: str | None):
|
|
35
|
+
"""Parse numpy-style index text like "0:8, :, 3" without eval."""
|
|
36
|
+
if index is None or not str(index).strip():
|
|
37
|
+
return ()
|
|
38
|
+
text = str(index).strip()
|
|
39
|
+
if text.startswith("[") and text.endswith("]"):
|
|
40
|
+
text = text[1:-1]
|
|
41
|
+
out = []
|
|
42
|
+
for part in text.split(","):
|
|
43
|
+
part = part.strip()
|
|
44
|
+
if part in ("", ":", "..."):
|
|
45
|
+
out.append(Ellipsis if part == "..." else slice(None))
|
|
46
|
+
continue
|
|
47
|
+
m = _SLICE_PART.match(part)
|
|
48
|
+
if not m:
|
|
49
|
+
raise ValueError(f"bad index component {part!r}")
|
|
50
|
+
if ":" not in part:
|
|
51
|
+
out.append(int(m.group(1)))
|
|
52
|
+
else:
|
|
53
|
+
a, b, c = (int(g) if g is not None else None for g in m.groups())
|
|
54
|
+
out.append(slice(a, b, c))
|
|
55
|
+
return tuple(out)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def stats(arr: np.ndarray) -> dict:
|
|
59
|
+
a = np.asarray(arr, dtype=np.float64).reshape(-1)
|
|
60
|
+
if a.size == 0:
|
|
61
|
+
return {"numel": 0}
|
|
62
|
+
q = np.quantile(a, [0.01, 0.25, 0.5, 0.75, 0.99])
|
|
63
|
+
return {
|
|
64
|
+
"numel": int(a.size),
|
|
65
|
+
"mean": float(a.mean()),
|
|
66
|
+
"std": float(a.std()),
|
|
67
|
+
"min": float(a.min()),
|
|
68
|
+
"max": float(a.max()),
|
|
69
|
+
"abs_mean": float(np.abs(a).mean()),
|
|
70
|
+
"frac_zero": float(np.mean(a == 0)),
|
|
71
|
+
"q01_q25_q50_q75_q99": [float(v) for v in q],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def describe(name: str, arr: np.ndarray, dtype: str = "", top_k: int = 8) -> str:
|
|
76
|
+
"""Structural summary: stats, row/col norms, spectrum for matrices."""
|
|
77
|
+
arr = np.asarray(arr)
|
|
78
|
+
s = stats(arr)
|
|
79
|
+
lines = [f"{name}: shape={tuple(arr.shape)} dtype={dtype or arr.dtype} numel={s['numel']:,}"]
|
|
80
|
+
if s["numel"] == 0:
|
|
81
|
+
return lines[0]
|
|
82
|
+
lines.append(
|
|
83
|
+
"mean={} std={} min={} max={} |x|mean={} zeros={:.3g}%".format(
|
|
84
|
+
*(fmt_num(s[k], 4) for k in ("mean", "std", "min", "max", "abs_mean")), 100 * s["frac_zero"]
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
lines.append("quantiles 1/25/50/75/99%: " + " ".join(fmt_num(v, 4) for v in s["q01_q25_q50_q75_q99"]))
|
|
88
|
+
if arr.ndim >= 2:
|
|
89
|
+
m = np.asarray(arr, dtype=np.float64).reshape(arr.shape[0], -1)
|
|
90
|
+
for axis, label in ((1, "row"), (0, "col")):
|
|
91
|
+
norms = np.linalg.norm(m, axis=axis)
|
|
92
|
+
order = np.argsort(norms)[::-1]
|
|
93
|
+
top = ", ".join(f"{i}:{fmt_num(norms[i], 3)}" for i in order[:top_k])
|
|
94
|
+
low = ", ".join(f"{i}:{fmt_num(norms[i], 3)}" for i in order[::-1][:top_k])
|
|
95
|
+
lines.append(f"{label} L2 norms ({norms.size}): largest {top} | smallest {low}")
|
|
96
|
+
if min(m.shape) <= 4096 and m.size <= 16_000_000:
|
|
97
|
+
sv = np.linalg.svd(m, compute_uv=False)
|
|
98
|
+
energy = np.cumsum(sv**2) / max(np.sum(sv**2), 1e-30)
|
|
99
|
+
eff = int(np.searchsorted(energy, 0.9) + 1)
|
|
100
|
+
lines.append(
|
|
101
|
+
f"singular values (top {min(top_k, sv.size)}): "
|
|
102
|
+
+ " ".join(fmt_num(v, 3) for v in sv[:top_k])
|
|
103
|
+
+ f" | rank for 90% energy: {eff}/{sv.size}"
|
|
104
|
+
)
|
|
105
|
+
return "\n".join(lines)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def render_space(ws, precision: int = 3) -> str:
|
|
109
|
+
"""Full text dump in the same format `parse_weights` reads back."""
|
|
110
|
+
parts = []
|
|
111
|
+
for n in ws.names():
|
|
112
|
+
arr = ws[n]
|
|
113
|
+
parts.append(f"### {n} {list(arr.shape)}\n{fmt_array(arr, precision)}")
|
|
114
|
+
return "\n\n".join(parts)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_HEADER = re.compile(r"^###\s+(\S+)(?:\s+\[([\d,\s]*)\])?\s*$")
|
|
118
|
+
_NUM = re.compile(r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def parse_weights(text: str) -> dict[str, np.ndarray]:
|
|
122
|
+
"""Parse `### name [shape]` blocks of numbers (row labels like [0,1] are ignored)."""
|
|
123
|
+
out: dict[str, np.ndarray] = {}
|
|
124
|
+
name, shape, nums = None, None, []
|
|
125
|
+
|
|
126
|
+
def done():
|
|
127
|
+
if name is None:
|
|
128
|
+
return
|
|
129
|
+
arr = np.array(nums, dtype=np.float32)
|
|
130
|
+
if shape is not None:
|
|
131
|
+
arr = arr.reshape(shape)
|
|
132
|
+
out[name] = arr
|
|
133
|
+
|
|
134
|
+
for line in text.splitlines():
|
|
135
|
+
m = _HEADER.match(line.strip())
|
|
136
|
+
if m:
|
|
137
|
+
done()
|
|
138
|
+
name = m.group(1)
|
|
139
|
+
shape = tuple(int(x) for x in m.group(2).split(",") if x.strip()) if m.group(2) is not None else None
|
|
140
|
+
nums = []
|
|
141
|
+
continue
|
|
142
|
+
if name is None:
|
|
143
|
+
continue
|
|
144
|
+
body = re.sub(r"^\s*\[[\d,\s]*\]", "", line)
|
|
145
|
+
nums.extend(float(x) for x in _NUM.findall(body))
|
|
146
|
+
done()
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def extract_blocks(text: str, lang: str) -> list[str]:
|
|
151
|
+
return [m.group(1) for m in re.finditer(rf"```{lang}[^\n]*\n(.*?)```", text, re.S)]
|