Library page with tested demos; no-agent veil; energy meters and J/token; 15 new skills; external MCP catalogue; py3.10 gate
Browse filesThis view is limited to 50 files because it contains too many changes. Β See raw diff
- .gitattributes +1 -0
- assets/library-demos/academy_guide-1.gif +3 -0
- assets/library-demos/algorithmic_art-1.gif +3 -0
- assets/library-demos/brand_guidelines-1.gif +3 -0
- assets/library-demos/canvas_design-1.gif +3 -0
- assets/library-demos/claude_api-1.gif +3 -0
- assets/library-demos/create_deck-1.gif +3 -0
- assets/library-demos/create_docx-1.gif +3 -0
- assets/library-demos/create_pdf-1.gif +3 -0
- assets/library-demos/create_xlsx-1.gif +3 -0
- assets/library-demos/csv_table-1.gif +3 -0
- assets/library-demos/discernment_nudge-1.gif +3 -0
- assets/library-demos/frontend_design-1.gif +3 -0
- assets/library-demos/make_plan-1.gif +3 -0
- assets/library-demos/mcp_builder-1.gif +3 -0
- assets/library-demos/review_checklist-1.gif +3 -0
- assets/library-demos/skill_scaffold-1.gif +3 -0
- assets/library-demos/slack_gif_creator-1.gif +3 -0
- assets/library-demos/status_update-1.gif +3 -0
- assets/library-demos/theme_factory-1.gif +3 -0
- assets/library-demos/web_artifacts_builder-1.gif +3 -0
- assets/library-demos/webapp_testing-1.gif +3 -0
- distinct_agent/README.md +17 -6
- distinct_agent/__main__.py +12 -1
- distinct_agent/cli.py +13 -1
- distinct_agent/energy.py +217 -0
- distinct_agent/models.py +93 -77
- distinct_mcp/external.py +275 -0
- distinct_server.egg-info/PKG-INFO +14 -0
- distinct_server.egg-info/SOURCES.txt +92 -0
- distinct_server.egg-info/dependency_links.txt +1 -0
- distinct_server.egg-info/requires.txt +13 -0
- distinct_server.egg-info/top_level.txt +6 -0
- distinct_server/catalog.py +264 -120
- distinct_server/presentation.py +123 -0
- distinct_server/render.py +94 -7
- distinct_server/ui.py +188 -53
- distinct_skills/handlers.py +638 -1
- distinct_skills/manifest.py +21 -2
- distinct_skills/skills/academy-guide/SKILL.md +15 -0
- distinct_skills/skills/academy-guide/assets/guide.md +23 -0
- distinct_skills/skills/academy-guide/tool.json +28 -0
- distinct_skills/skills/algorithmic-art/SKILL.md +15 -0
- distinct_skills/skills/algorithmic-art/assets/guide.md +24 -0
- distinct_skills/skills/algorithmic-art/tool.json +28 -0
- distinct_skills/skills/brand-guidelines/SKILL.md +15 -0
- distinct_skills/skills/brand-guidelines/assets/guide.md +17 -0
- distinct_skills/skills/brand-guidelines/tool.json +28 -0
- distinct_skills/skills/canvas-design/SKILL.md +15 -0
- distinct_skills/skills/canvas-design/assets/guide.md +24 -0
.gitattributes
CHANGED
|
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
*.webp filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.gif filter=lfs diff=lfs merge=lfs -text
|
assets/library-demos/academy_guide-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/algorithmic_art-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/brand_guidelines-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/canvas_design-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/claude_api-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/create_deck-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/create_docx-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/create_pdf-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/create_xlsx-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/csv_table-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/discernment_nudge-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/frontend_design-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/make_plan-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/mcp_builder-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/review_checklist-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/skill_scaffold-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/slack_gif_creator-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/status_update-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/theme_factory-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/web_artifacts_builder-1.gif
ADDED
|
Git LFS Details
|
assets/library-demos/webapp_testing-1.gif
ADDED
|
Git LFS Details
|
distinct_agent/README.md
CHANGED
|
@@ -60,21 +60,32 @@ whole PC. Unsupported and failed measurements contain `available: false` and
|
|
| 60 |
|
| 61 |
## Models and runner
|
| 62 |
|
| 63 |
-
Discovery recognizes local GGUF files for
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
An explicit deterministic smoke test is:
|
| 69 |
|
| 70 |
```console
|
| 71 |
-
python -m distinct_agent --model
|
| 72 |
```
|
| 73 |
|
| 74 |
To pair with a server, log in there, generate a one-use code, and run:
|
| 75 |
|
| 76 |
```console
|
| 77 |
-
python -m distinct_agent --server https://owner-space.hf.space --pair XXXX-XXXX-XXXX-XXXX --name "My efficient worker" --model
|
| 78 |
```
|
| 79 |
|
| 80 |
The default harness is `dspy-rlm`; startup performs its cached-only
|
|
|
|
| 60 |
|
| 61 |
## Models and runner
|
| 62 |
|
| 63 |
+
Discovery recognizes local GGUF files for OLMoE-1B-7B-0924-Instruct,
|
| 64 |
+
OLMo-2-1124-7B-Instruct and Llama-2-7B-Chat. It performs no download and
|
| 65 |
+
executes no remote model code. `LlamaCppRunner` invokes an installed
|
| 66 |
+
`llama-cli` with an argument array and `shell=False`.
|
| 67 |
+
|
| 68 |
+
The catalogue is short on purpose. Every model in it has a published,
|
| 69 |
+
model-specific assessment of its training, recorded with its primary source in
|
| 70 |
+
`model_assessments.json`; a release nobody has published anything about is not
|
| 71 |
+
offered, because measured and disclosed environmental cost is the only claim
|
| 72 |
+
this project makes. The two OLMo entries are first party end to end -- Ai2
|
| 73 |
+
trained the models, measured the training, quantised the weights and published
|
| 74 |
+
the digests -- and both are pinned by repository, revision and SHA-256. Llama 2
|
| 75 |
+
is listed and withheld: Meta publish no GGUF and the source repository is
|
| 76 |
+
gated, so there is no digest to pin, and the worker names it as unavailable on
|
| 77 |
+
every start rather than dropping it silently.
|
| 78 |
|
| 79 |
An explicit deterministic smoke test is:
|
| 80 |
|
| 81 |
```console
|
| 82 |
+
python -m distinct_agent --model olmoe-1b-7b-0924-instruct=/models/olmoe.gguf --demo-prompt "hello"
|
| 83 |
```
|
| 84 |
|
| 85 |
To pair with a server, log in there, generate a one-use code, and run:
|
| 86 |
|
| 87 |
```console
|
| 88 |
+
python -m distinct_agent --server https://owner-space.hf.space --pair XXXX-XXXX-XXXX-XXXX --name "My efficient worker" --model olmoe-1b-7b-0924-instruct=/models/olmoe.gguf --llama-cli /opt/llama/llama-cli
|
| 89 |
```
|
| 90 |
|
| 91 |
The default harness is `dspy-rlm`; startup performs its cached-only
|
distinct_agent/__main__.py
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from .cli import main
|
| 2 |
|
| 3 |
raise SystemExit(main())
|
| 4 |
-
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
# The same gate cli.py carries, one frame earlier: ``python -m distinct_agent``
|
| 4 |
+
# on a pre-3.10 interpreter must say so before any project import runs.
|
| 5 |
+
if sys.version_info < (3, 10):
|
| 6 |
+
sys.exit(
|
| 7 |
+
"distinct needs Python 3.10 or newer; this is "
|
| 8 |
+
f"{sys.version_info.major}.{sys.version_info.minor} ({sys.executable}).\n"
|
| 9 |
+
"On Windows, run: py -3.12 -m distinct_agent ...\n"
|
| 10 |
+
"Elsewhere, run: python3.12 -m distinct_agent ..."
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
from .cli import main
|
| 14 |
|
| 15 |
raise SystemExit(main())
|
|
|
distinct_agent/cli.py
CHANGED
|
@@ -18,6 +18,18 @@ from dataclasses import replace
|
|
| 18 |
from pathlib import Path
|
| 19 |
from urllib.parse import urlsplit
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
from distinct_protocol import (
|
| 22 |
MODE_AGENT,
|
| 23 |
MODE_SIMPLE,
|
|
@@ -42,6 +54,7 @@ from distinct_tools import (
|
|
| 42 |
)
|
| 43 |
from distinct_tools.approval import APPROVED_TOOLS_ENV_VAR
|
| 44 |
|
|
|
|
| 45 |
from .energy import detect_energy_meter
|
| 46 |
from .fanout import MultiServerTransport, ServerLink
|
| 47 |
from .isolation import describe_request_isolation, describe_support
|
|
@@ -59,7 +72,6 @@ from .servers import (
|
|
| 59 |
ServerRegistry,
|
| 60 |
)
|
| 61 |
from .tools import advertised_tool_refs
|
| 62 |
-
from .access import announce_access_code, new_access_code
|
| 63 |
from .transport import (
|
| 64 |
AgentTransportError,
|
| 65 |
GradioAgentTransport,
|
|
|
|
| 18 |
from pathlib import Path
|
| 19 |
from urllib.parse import urlsplit
|
| 20 |
|
| 21 |
+
# Before any project import: the project uses 3.10 syntax at module level
|
| 22 |
+
# (PEP 604 unions in isinstance), so on an older interpreter the failure is
|
| 23 |
+
# an unrelated-looking TypeError deep in an import. A person on Windows whose
|
| 24 |
+
# ``python`` is a stray 3.9 deserves the actual sentence instead.
|
| 25 |
+
if sys.version_info < (3, 10): # pragma: no cover - cannot run under test
|
| 26 |
+
sys.exit(
|
| 27 |
+
"distinct needs Python 3.10 or newer; this is "
|
| 28 |
+
f"{sys.version_info.major}.{sys.version_info.minor} ({sys.executable}).\n"
|
| 29 |
+
"On Windows, run: py -3.12 -m distinct_agent ...\n"
|
| 30 |
+
"Elsewhere, run: python3.12 -m distinct_agent ..."
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
from distinct_protocol import (
|
| 34 |
MODE_AGENT,
|
| 35 |
MODE_SIMPLE,
|
|
|
|
| 54 |
)
|
| 55 |
from distinct_tools.approval import APPROVED_TOOLS_ENV_VAR
|
| 56 |
|
| 57 |
+
from .access import announce_access_code, new_access_code
|
| 58 |
from .energy import detect_energy_meter
|
| 59 |
from .fanout import MultiServerTransport, ServerLink
|
| 60 |
from .isolation import describe_request_isolation, describe_support
|
|
|
|
| 72 |
ServerRegistry,
|
| 73 |
)
|
| 74 |
from .tools import advertised_tool_refs
|
|
|
|
| 75 |
from .transport import (
|
| 76 |
AgentTransportError,
|
| 77 |
GradioAgentTransport,
|
distinct_agent/energy.py
CHANGED
|
@@ -1052,6 +1052,200 @@ class ZeusEnergyMeter:
|
|
| 1052 |
)
|
| 1053 |
|
| 1054 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
def detect_energy_meter(*, system: str | None = None) -> EnergyMeter:
|
| 1056 |
"""Choose a backend: measured counters first, the stated model last.
|
| 1057 |
|
|
@@ -1073,6 +1267,20 @@ def detect_energy_meter(*, system: str | None = None) -> EnergyMeter:
|
|
| 1073 |
zeus = ZeusEnergyMeter()
|
| 1074 |
if zeus.available:
|
| 1075 |
return zeus
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
if operating_system == "Linux":
|
| 1077 |
# Direct powercap first: same hardware counter pyRAPL wraps, without
|
| 1078 |
# importing an abandoned package that logs warnings at import time.
|
|
@@ -1082,6 +1290,12 @@ def detect_energy_meter(*, system: str | None = None) -> EnergyMeter:
|
|
| 1082 |
pyrapl = PyRaplEnergyMeter()
|
| 1083 |
if pyrapl.available:
|
| 1084 |
return pyrapl
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1085 |
model = CpuLoadModelMeter()
|
| 1086 |
if model.available:
|
| 1087 |
return model
|
|
@@ -1095,6 +1309,9 @@ def detect_energy_meter(*, system: str | None = None) -> EnergyMeter:
|
|
| 1095 |
counter = WindowsPerformanceCounterRaplMeter()
|
| 1096 |
if counter.available:
|
| 1097 |
return counter
|
|
|
|
|
|
|
|
|
|
| 1098 |
# Modelled, and labelled as modelled. A Windows worker with no usable
|
| 1099 |
# counter used to report nothing at all, which made every one of its
|
| 1100 |
# runs an absence; the model is a real number about a real run and it
|
|
|
|
| 1052 |
)
|
| 1053 |
|
| 1054 |
|
| 1055 |
+
|
| 1056 |
+
class NvidiaSmiPowerMeter:
|
| 1057 |
+
"""Measured GPU board power, integrated from ``nvidia-smi`` samples.
|
| 1058 |
+
|
| 1059 |
+
``nvidia-smi --query-gpu=power.draw`` reports the board's instantaneous
|
| 1060 |
+
draw in watts from the driver's own sensor. This meter samples it on a
|
| 1061 |
+
small interval for the life of a run and integrates by the trapezoid
|
| 1062 |
+
rule, which turns a power series into joules without a vendor library:
|
| 1063 |
+
the same figure ``nvml``'s energy counter gives on hardware that has one,
|
| 1064 |
+
obtained portably on hardware that does not.
|
| 1065 |
+
|
| 1066 |
+
The scope is the whole GPU board β memory, fans and idle draw included β
|
| 1067 |
+
which is broader than a compute-only figure and is named so it can never
|
| 1068 |
+
be summed with a cpu-package number. Sampling costs one subprocess per
|
| 1069 |
+
interval; 250 ms keeps that below the noise floor of inference itself.
|
| 1070 |
+
"""
|
| 1071 |
+
|
| 1072 |
+
provider = "nvidia-smi"
|
| 1073 |
+
scope = "gpu-board-measured"
|
| 1074 |
+
|
| 1075 |
+
_INTERVAL_SECONDS = 0.25
|
| 1076 |
+
|
| 1077 |
+
def __init__(self, *, executable: str | None = None) -> None:
|
| 1078 |
+
import shutil as _shutil
|
| 1079 |
+
|
| 1080 |
+
self._meter_id = uuid.uuid4().hex
|
| 1081 |
+
self._executable = executable or _shutil.which("nvidia-smi") or ""
|
| 1082 |
+
self._sessions: dict[str, dict[str, object]] = {}
|
| 1083 |
+
self._available = False
|
| 1084 |
+
self._reason: str | None = None
|
| 1085 |
+
if not self._executable:
|
| 1086 |
+
self._reason = "nvidia-smi is not on PATH"
|
| 1087 |
+
return
|
| 1088 |
+
sample = self._sample_watts()
|
| 1089 |
+
if sample is None:
|
| 1090 |
+
self._reason = "nvidia-smi answered without a readable power figure"
|
| 1091 |
+
return
|
| 1092 |
+
self._available = True
|
| 1093 |
+
|
| 1094 |
+
@property
|
| 1095 |
+
def available(self) -> bool:
|
| 1096 |
+
return self._available
|
| 1097 |
+
|
| 1098 |
+
@property
|
| 1099 |
+
def unavailable_reason(self) -> str | None:
|
| 1100 |
+
return self._reason
|
| 1101 |
+
|
| 1102 |
+
def _sample_watts(self) -> float | None:
|
| 1103 |
+
import subprocess
|
| 1104 |
+
|
| 1105 |
+
try:
|
| 1106 |
+
completed = subprocess.run(
|
| 1107 |
+
[
|
| 1108 |
+
self._executable,
|
| 1109 |
+
"--query-gpu=power.draw",
|
| 1110 |
+
"--format=csv,noheader,nounits",
|
| 1111 |
+
],
|
| 1112 |
+
capture_output=True,
|
| 1113 |
+
text=True,
|
| 1114 |
+
timeout=3,
|
| 1115 |
+
check=False,
|
| 1116 |
+
)
|
| 1117 |
+
except (OSError, subprocess.SubprocessError):
|
| 1118 |
+
return None
|
| 1119 |
+
if completed.returncode != 0:
|
| 1120 |
+
return None
|
| 1121 |
+
total = 0.0
|
| 1122 |
+
seen = False
|
| 1123 |
+
for line in completed.stdout.splitlines():
|
| 1124 |
+
line = line.strip()
|
| 1125 |
+
if not line:
|
| 1126 |
+
continue
|
| 1127 |
+
try:
|
| 1128 |
+
watts = float(line)
|
| 1129 |
+
except ValueError:
|
| 1130 |
+
continue
|
| 1131 |
+
if math.isfinite(watts) and 0.0 <= watts <= 10_000.0:
|
| 1132 |
+
total += watts
|
| 1133 |
+
seen = True
|
| 1134 |
+
return total if seen else None
|
| 1135 |
+
|
| 1136 |
+
def start(self) -> EnergyToken:
|
| 1137 |
+
token = EnergyToken(
|
| 1138 |
+
meter_id=self._meter_id,
|
| 1139 |
+
provider=self.provider,
|
| 1140 |
+
scope=self.scope,
|
| 1141 |
+
started_at=time.monotonic(),
|
| 1142 |
+
)
|
| 1143 |
+
if not self._available:
|
| 1144 |
+
return token
|
| 1145 |
+
stop = threading.Event()
|
| 1146 |
+
session: dict[str, object] = {"stop": stop, "joules": 0.0, "samples": 0}
|
| 1147 |
+
|
| 1148 |
+
def poll() -> None:
|
| 1149 |
+
last_time = time.monotonic()
|
| 1150 |
+
last_watts = self._sample_watts()
|
| 1151 |
+
while not stop.wait(self._INTERVAL_SECONDS):
|
| 1152 |
+
now = time.monotonic()
|
| 1153 |
+
watts = self._sample_watts()
|
| 1154 |
+
if watts is not None and last_watts is not None:
|
| 1155 |
+
session["joules"] = (
|
| 1156 |
+
float(session["joules"])
|
| 1157 |
+
+ (watts + last_watts) / 2.0 * (now - last_time)
|
| 1158 |
+
)
|
| 1159 |
+
session["samples"] = int(session["samples"]) + 1
|
| 1160 |
+
last_time, last_watts = now, watts
|
| 1161 |
+
|
| 1162 |
+
thread = threading.Thread(target=poll, name="nvidia-smi-energy", daemon=True)
|
| 1163 |
+
session["thread"] = thread
|
| 1164 |
+
self._sessions[token.token_id] = session
|
| 1165 |
+
thread.start()
|
| 1166 |
+
return token
|
| 1167 |
+
|
| 1168 |
+
def stop(self, token: EnergyToken) -> EnergyUsage:
|
| 1169 |
+
_check_token_owner(token, self._meter_id)
|
| 1170 |
+
duration = max(0.0, time.monotonic() - token.started_at)
|
| 1171 |
+
session = self._sessions.pop(token.token_id, None)
|
| 1172 |
+
if not self._available or session is None:
|
| 1173 |
+
return _unavailable_usage(token, duration)
|
| 1174 |
+
stop = session["stop"]
|
| 1175 |
+
assert isinstance(stop, threading.Event)
|
| 1176 |
+
stop.set()
|
| 1177 |
+
thread = session.get("thread")
|
| 1178 |
+
if isinstance(thread, threading.Thread):
|
| 1179 |
+
thread.join(timeout=2.0)
|
| 1180 |
+
if int(session["samples"]) == 0:
|
| 1181 |
+
return EnergyUsage(
|
| 1182 |
+
self.provider,
|
| 1183 |
+
self.scope,
|
| 1184 |
+
available=False,
|
| 1185 |
+
joules=None,
|
| 1186 |
+
duration_seconds=duration,
|
| 1187 |
+
reason="run ended before a second power sample arrived",
|
| 1188 |
+
)
|
| 1189 |
+
return EnergyUsage(
|
| 1190 |
+
self.provider,
|
| 1191 |
+
self.scope,
|
| 1192 |
+
available=True,
|
| 1193 |
+
joules=float(session["joules"]),
|
| 1194 |
+
duration_seconds=duration,
|
| 1195 |
+
)
|
| 1196 |
+
|
| 1197 |
+
|
| 1198 |
+
class PowermetricsProbeMeter:
|
| 1199 |
+
"""Apple Silicon: names the working path rather than faking one.
|
| 1200 |
+
|
| 1201 |
+
macOS exposes package power through ``powermetrics``, which requires
|
| 1202 |
+
root, and through the undocumented counters ``zeus-apple-silicon``
|
| 1203 |
+
wraps. When neither is usable this meter stays unavailable with the
|
| 1204 |
+
reason spelled out, so a Mac worker reports why its figure is absent
|
| 1205 |
+
instead of a bare absence. Zeus (tried before this in the ladder)
|
| 1206 |
+
remains the measured path when its Apple support is installed.
|
| 1207 |
+
"""
|
| 1208 |
+
|
| 1209 |
+
provider = "powermetrics"
|
| 1210 |
+
scope = "cpu-package"
|
| 1211 |
+
|
| 1212 |
+
def __init__(self) -> None:
|
| 1213 |
+
import shutil as _shutil
|
| 1214 |
+
|
| 1215 |
+
self._meter_id = uuid.uuid4().hex
|
| 1216 |
+
self._available = False
|
| 1217 |
+
located = _shutil.which("powermetrics")
|
| 1218 |
+
if not located:
|
| 1219 |
+
self._reason = "powermetrics is not on PATH (macOS ships it in /usr/bin)"
|
| 1220 |
+
else:
|
| 1221 |
+
# Running it needs root; probing with sudo would prompt, and a
|
| 1222 |
+
# meter must never prompt. State the requirement instead.
|
| 1223 |
+
self._reason = (
|
| 1224 |
+
"powermetrics needs root. Run the worker with sudo, or install "
|
| 1225 |
+
"zeus-apple-silicon for the unprivileged counter path"
|
| 1226 |
+
)
|
| 1227 |
+
|
| 1228 |
+
@property
|
| 1229 |
+
def available(self) -> bool:
|
| 1230 |
+
return self._available
|
| 1231 |
+
|
| 1232 |
+
@property
|
| 1233 |
+
def unavailable_reason(self) -> str | None:
|
| 1234 |
+
return self._reason
|
| 1235 |
+
|
| 1236 |
+
def start(self) -> EnergyToken:
|
| 1237 |
+
return EnergyToken(
|
| 1238 |
+
meter_id=self._meter_id,
|
| 1239 |
+
provider=self.provider,
|
| 1240 |
+
scope=self.scope,
|
| 1241 |
+
started_at=time.monotonic(),
|
| 1242 |
+
)
|
| 1243 |
+
|
| 1244 |
+
def stop(self, token: EnergyToken) -> EnergyUsage:
|
| 1245 |
+
_check_token_owner(token, self._meter_id)
|
| 1246 |
+
return _unavailable_usage(token, max(0.0, time.monotonic() - token.started_at))
|
| 1247 |
+
|
| 1248 |
+
|
| 1249 |
def detect_energy_meter(*, system: str | None = None) -> EnergyMeter:
|
| 1250 |
"""Choose a backend: measured counters first, the stated model last.
|
| 1251 |
|
|
|
|
| 1267 |
zeus = ZeusEnergyMeter()
|
| 1268 |
if zeus.available:
|
| 1269 |
return zeus
|
| 1270 |
+
if operating_system == "Darwin":
|
| 1271 |
+
# Zeus above already covers a Mac with zeus-apple-silicon installed.
|
| 1272 |
+
# Below it, powermetrics is the OS's own meter but needs root; the
|
| 1273 |
+
# probe meter names that requirement, and the load model keeps a Mac
|
| 1274 |
+
# worker reporting a labelled figure rather than nothing.
|
| 1275 |
+
probe = PowermetricsProbeMeter()
|
| 1276 |
+
if probe.available:
|
| 1277 |
+
return probe
|
| 1278 |
+
model = CpuLoadModelMeter()
|
| 1279 |
+
if model.available:
|
| 1280 |
+
return model
|
| 1281 |
+
return UnavailableEnergyMeter(
|
| 1282 |
+
probe.unavailable_reason or "no macOS energy backend is available"
|
| 1283 |
+
)
|
| 1284 |
if operating_system == "Linux":
|
| 1285 |
# Direct powercap first: same hardware counter pyRAPL wraps, without
|
| 1286 |
# importing an abandoned package that logs warnings at import time.
|
|
|
|
| 1290 |
pyrapl = PyRaplEnergyMeter()
|
| 1291 |
if pyrapl.available:
|
| 1292 |
return pyrapl
|
| 1293 |
+
# A machine whose inference runs on the GPU: the board figure is the
|
| 1294 |
+
# honest one when the CPU counters are fenced off (as they are in
|
| 1295 |
+
# containers), and it is measured rather than modelled.
|
| 1296 |
+
smi = NvidiaSmiPowerMeter()
|
| 1297 |
+
if smi.available:
|
| 1298 |
+
return smi
|
| 1299 |
model = CpuLoadModelMeter()
|
| 1300 |
if model.available:
|
| 1301 |
return model
|
|
|
|
| 1309 |
counter = WindowsPerformanceCounterRaplMeter()
|
| 1310 |
if counter.available:
|
| 1311 |
return counter
|
| 1312 |
+
smi = NvidiaSmiPowerMeter()
|
| 1313 |
+
if smi.available:
|
| 1314 |
+
return smi
|
| 1315 |
# Modelled, and labelled as modelled. A Windows worker with no usable
|
| 1316 |
# counter used to report nothing at all, which made every one of its
|
| 1317 |
# runs an absence; the model is a real number about a real run and it
|
distinct_agent/models.py
CHANGED
|
@@ -27,12 +27,16 @@ class UnpinnedModelError(ModelVerificationError):
|
|
| 27 |
# A model is advertised only after its file is found locally. Presence in this
|
| 28 |
# catalogue never means usable.
|
| 29 |
#
|
| 30 |
-
# PUBLISHER POLICY.
|
| 31 |
-
#
|
| 32 |
-
#
|
| 33 |
-
#
|
| 34 |
-
#
|
| 35 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
#
|
| 37 |
# DIGESTS. A digest is meaningless without the repository and revision it came
|
| 38 |
# from: the same nominal quantisation published twice is two different files.
|
|
@@ -40,10 +44,6 @@ class UnpinnedModelError(ModelVerificationError):
|
|
| 40 |
# pending, and an empty digest is an unanswered question rather than a passing
|
| 41 |
# check; `discover_models(verify_hashes=True)` refuses them loudly.
|
| 42 |
#
|
| 43 |
-
# Llama-2-7B-Chat has no unsloth GGUF (their Llama-2 publications are
|
| 44 |
-
# safetensors and bitsandbytes only), so it stays unpinned pending a decision
|
| 45 |
-
# on whether to convert locally or accept a third-party GGUF.
|
| 46 |
-
#
|
| 47 |
# WHAT AN UNPINNED ENTRY NOW MEANS. Weights are fetched on demand rather than
|
| 48 |
# placed on disk by the operator, so a manifest is only usable when its
|
| 49 |
# repository, revision *and* digest are all recorded: without a revision there
|
|
@@ -51,78 +51,98 @@ class UnpinnedModelError(ModelVerificationError):
|
|
| 51 |
# was run. :func:`distinct_agent.weights.fetchable_manifests` filters this list
|
| 52 |
# to the entries that satisfy all three, and a worker offers nothing else.
|
| 53 |
#
|
| 54 |
-
# The unpinned
|
| 55 |
-
#
|
| 56 |
-
#
|
| 57 |
-
#
|
| 58 |
-
#
|
| 59 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
KNOWN_MODEL_MANIFESTS: tuple[ModelManifest, ...] = (
|
| 61 |
-
# DEFAULT. Chosen on
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
#
|
| 63 |
-
#
|
| 64 |
-
#
|
| 65 |
-
#
|
| 66 |
-
#
|
| 67 |
-
#
|
| 68 |
-
#
|
|
|
|
| 69 |
ModelManifest(
|
| 70 |
-
id="
|
| 71 |
-
label="
|
| 72 |
-
filename="
|
| 73 |
-
min_ram_gb=
|
| 74 |
-
sha256="
|
| 75 |
-
source_repo="allenai/
|
| 76 |
-
source_revision="
|
| 77 |
context_length=4096,
|
| 78 |
),
|
| 79 |
-
#
|
| 80 |
-
#
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
# Verified by downloading this exact revision and hashing the file;
|
| 87 |
-
# see scripts/fetch_runtime.py.
|
| 88 |
-
sha256="ac2d97712095a558e31573f62f466a3f9d93990898b0ec79d7c974c1780d524a",
|
| 89 |
-
source_repo="unsloth/Qwen3-0.6B-GGUF",
|
| 90 |
-
source_revision="50968a4468ef4233ed78cd7c3de230dd1d61a56b",
|
| 91 |
-
context_length=40960,
|
| 92 |
-
),
|
| 93 |
-
# OLMoE: first-party GGUF, and Ai2 tag these repos with co2_eq_emissions,
|
| 94 |
-
# which is the disclosure the rubric rewards. Digest pending; the repo and
|
| 95 |
-
# revision are recorded so it can be pinned without another search.
|
| 96 |
ModelManifest(
|
| 97 |
-
id="
|
| 98 |
-
label="
|
| 99 |
-
filename="
|
| 100 |
min_ram_gb=8.0,
|
| 101 |
-
|
|
|
|
|
|
|
| 102 |
context_length=4096,
|
| 103 |
),
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
| 112 |
ModelManifest(
|
| 113 |
id="llama-2-7b-chat",
|
| 114 |
-
label="Llama
|
| 115 |
filename="llama-2-7b-chat.Q4_K_M.gguf",
|
| 116 |
min_ram_gb=8.0,
|
| 117 |
context_length=4096,
|
| 118 |
),
|
| 119 |
-
ModelManifest(
|
| 120 |
-
id="gpt-oss-20b",
|
| 121 |
-
label="gpt-oss-20b Β· MXFP4",
|
| 122 |
-
filename="gpt-oss-20b-mxfp4.gguf",
|
| 123 |
-
min_ram_gb=16.0,
|
| 124 |
-
context_length=131072,
|
| 125 |
-
),
|
| 126 |
)
|
| 127 |
|
| 128 |
|
|
@@ -131,17 +151,13 @@ _FILENAME_ALIASES: Mapping[str, tuple[str, ...]] = {
|
|
| 131 |
"llama-2-7b-chat.Q4_K_M.gguf",
|
| 132 |
"Llama-2-7B-Chat.Q4_K_M.gguf",
|
| 133 |
),
|
| 134 |
-
"
|
| 135 |
-
"
|
| 136 |
-
"
|
| 137 |
-
),
|
| 138 |
-
"qwen3-0.6b": (
|
| 139 |
-
"Qwen3-0.6B-Q4_K_M.gguf",
|
| 140 |
-
"qwen3-0.6b-q4_k_m.gguf",
|
| 141 |
),
|
| 142 |
-
"
|
| 143 |
-
"
|
| 144 |
-
"
|
| 145 |
),
|
| 146 |
}
|
| 147 |
|
|
|
|
| 27 |
# A model is advertised only after its file is found locally. Presence in this
|
| 28 |
# catalogue never means usable.
|
| 29 |
#
|
| 30 |
+
# PUBLISHER POLICY. Weights are taken from the organisation that trained them,
|
| 31 |
+
# and from nobody else. The earlier policy defaulted to unsloth's GGUF
|
| 32 |
+
# republications on the argument that a smaller file for the same nominal
|
| 33 |
+
# quantisation is less to download and less to hold in memory. That argument is
|
| 34 |
+
# still true and it is still not the deciding one: a digest attests to whoever
|
| 35 |
+
# built the file, so a third-party requantisation pins the repackager rather
|
| 36 |
+
# than the trainer, and this catalogue's whole claim is that you can check who
|
| 37 |
+
# made what you are running. Ai2 publish their own GGUFs, which is what makes
|
| 38 |
+
# the entries below first party end to end -- the same organisation trained the
|
| 39 |
+
# model, measured its training, quantised it and published the digest.
|
| 40 |
#
|
| 41 |
# DIGESTS. A digest is meaningless without the repository and revision it came
|
| 42 |
# from: the same nominal quantisation published twice is two different files.
|
|
|
|
| 44 |
# pending, and an empty digest is an unanswered question rather than a passing
|
| 45 |
# check; `discover_models(verify_hashes=True)` refuses them loudly.
|
| 46 |
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
# WHAT AN UNPINNED ENTRY NOW MEANS. Weights are fetched on demand rather than
|
| 48 |
# placed on disk by the operator, so a manifest is only usable when its
|
| 49 |
# repository, revision *and* digest are all recorded: without a revision there
|
|
|
|
| 51 |
# was run. :func:`distinct_agent.weights.fetchable_manifests` filters this list
|
| 52 |
# to the entries that satisfy all three, and a worker offers nothing else.
|
| 53 |
#
|
| 54 |
+
# The unpinned entry stays here rather than being deleted. It is the record of
|
| 55 |
+
# what is missing and for which model, it keeps the assessment surface able to
|
| 56 |
+
# name a release it has assessed but cannot run, and completing it is then a
|
| 57 |
+
# data change (three strings) rather than an archaeology exercise. The worker
|
| 58 |
+
# prints every withheld model and its missing part on every start, so an
|
| 59 |
+
# absence is never silent.
|
| 60 |
+
#
|
| 61 |
+
# WHY THESE THREE. Every entry here has a published, model-specific assessment
|
| 62 |
+
# of its training, recorded in ``model_assessments.json`` with its primary
|
| 63 |
+
# source. Models with no such assessment were removed rather than shown with an
|
| 64 |
+
# empty record: this project's only claim is about disclosed and measured
|
| 65 |
+
# environmental cost, and a model nobody has published anything about is one it
|
| 66 |
+
# has no business shipping. What was removed, and why:
|
| 67 |
+
#
|
| 68 |
+
# Qwen3 0.6B and 4B Qwen has published nothing for any release, in any
|
| 69 |
+
# version. Previously kept as the worked example of zero
|
| 70 |
+
# disclosure; that example now lives in the assessment
|
| 71 |
+
# surface's own copy, which does not require shipping the
|
| 72 |
+
# weights to make the point.
|
| 73 |
+
# gpt-oss-20b No disclosure of any kind on the model card or
|
| 74 |
+
# elsewhere.
|
| 75 |
+
# OLMo 2 0425 1B Released a month after Ai2's assessment closed. Ai2 have
|
| 76 |
+
# published nothing for it since, and a figure for a
|
| 77 |
+
# different OLMo checkpoint is not a figure for this one.
|
| 78 |
+
# OLMoE 0125 Superseded here by OLMoE 0924, which is the checkpoint
|
| 79 |
+
# Ai2 actually measured. Same architecture, same parameter
|
| 80 |
+
# count, four months apart -- and by this catalogue's own
|
| 81 |
+
# rule a different version is a different model, so the
|
| 82 |
+
# date stamp is the entire difference between an entry
|
| 83 |
+
# with evidence and an entry without.
|
| 84 |
KNOWN_MODEL_MANIFESTS: tuple[ModelManifest, ...] = (
|
| 85 |
+
# DEFAULT. Chosen on published, measured environmental impact, not on
|
| 86 |
+
# capability, and not on file size.
|
| 87 |
+
#
|
| 88 |
+
# Ai2 measured this run: GPU power sampled at sub-second intervals rather
|
| 89 |
+
# than inferred from rated wattage, giving 54 MWh, 18 tCO2e and 70 kL of
|
| 90 |
+
# water for the pretraining run. That is the lowest published figure of any
|
| 91 |
+
# assessed release at a size this network can run, and it is roughly a third
|
| 92 |
+
# of the dense OLMo 2 7B measured by the same team on the same cluster.
|
| 93 |
+
# Mixture of experts: 6.9B parameters resident, about 1.3B active per token.
|
| 94 |
#
|
| 95 |
+
# See model_assessments.json, record ``olmoe-1b-7b-0924``, and
|
| 96 |
+
# https://arxiv.org/abs/2503.05804 for the measurement.
|
| 97 |
+
#
|
| 98 |
+
# NOTE ON COVERAGE. The published figures cover the 0924 *pretraining* run.
|
| 99 |
+
# This entry is the Instruct checkpoint, whose tuning stage the source does
|
| 100 |
+
# not separately cost. The assessment record says so; do not quietly promote
|
| 101 |
+
# it to complete coverage.
|
| 102 |
ModelManifest(
|
| 103 |
+
id="olmoe-1b-7b-0924-instruct",
|
| 104 |
+
label="OLMoE 1B-7B 0924 Instruct Β· Ai2 Β· Q4_K_M",
|
| 105 |
+
filename="olmoe-1b-7b-0924-instruct-q4_k_m.gguf",
|
| 106 |
+
min_ram_gb=8.0,
|
| 107 |
+
sha256="8c310f1435a1222338fd2d3d974975be9cd908180b644bab0c2a94da1ac32f3f",
|
| 108 |
+
source_repo="allenai/OLMoE-1B-7B-0924-Instruct-GGUF",
|
| 109 |
+
source_revision="02ab6ea6894a8418eb14f6d8ee1bfb08bd298080",
|
| 110 |
context_length=4096,
|
| 111 |
),
|
| 112 |
+
# The dense comparison, and the fallback if the MoE architecture misbehaves
|
| 113 |
+
# under a given llama.cpp build. Same team, same cluster, same measurement
|
| 114 |
+
# method, same year, so the gap between this and OLMoE above is one of the
|
| 115 |
+
# few genuinely like-for-like comparisons in this literature: 157 MWh,
|
| 116 |
+
# 52 tCO2e, 202 kL. Roughly three times OLMoE on every disclosed axis.
|
| 117 |
+
#
|
| 118 |
+
# See model_assessments.json, record ``olmo-2-1124-7b``.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
ModelManifest(
|
| 120 |
+
id="olmo-2-1124-7b-instruct",
|
| 121 |
+
label="OLMo 2 1124 7B Instruct Β· Ai2 Β· Q4_K_M",
|
| 122 |
+
filename="olmo-2-1124-7B-instruct-Q4_K_M.gguf",
|
| 123 |
min_ram_gb=8.0,
|
| 124 |
+
sha256="e08112e5f84aab7c05fa6e713c58e5214cd5d8e32ed773ff3354b006eed41b95",
|
| 125 |
+
source_repo="allenai/OLMo-2-1124-7B-Instruct-GGUF",
|
| 126 |
+
source_revision="410e0069f64869e4b1d17d8de04810b881fd824b",
|
| 127 |
context_length=4096,
|
| 128 |
),
|
| 129 |
+
# Kept as the contrast case: the only non-Ai2 release in this catalogue with
|
| 130 |
+
# a published model-specific figure, and a partial one. Meta published
|
| 131 |
+
# GPU-hours and a modelled 31.22 tCO2e; no energy, no water, no materials.
|
| 132 |
+
#
|
| 133 |
+
# Still unpinned, and for two reasons that are worth keeping visible rather
|
| 134 |
+
# than resolving quietly. Meta publish no GGUF, so any file would be a third
|
| 135 |
+
# party's, which the publisher policy above rules out. And the source
|
| 136 |
+
# repository is gated, so there is no unauthenticated revision to pin even
|
| 137 |
+
# if that policy changed. ``fetchable_manifests`` therefore withholds it and
|
| 138 |
+
# the worker says so on every start. It is here to be named, not run.
|
| 139 |
ModelManifest(
|
| 140 |
id="llama-2-7b-chat",
|
| 141 |
+
label="Llama 2 7B Chat Β· Meta Β· gated licence",
|
| 142 |
filename="llama-2-7b-chat.Q4_K_M.gguf",
|
| 143 |
min_ram_gb=8.0,
|
| 144 |
context_length=4096,
|
| 145 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
)
|
| 147 |
|
| 148 |
|
|
|
|
| 151 |
"llama-2-7b-chat.Q4_K_M.gguf",
|
| 152 |
"Llama-2-7B-Chat.Q4_K_M.gguf",
|
| 153 |
),
|
| 154 |
+
"olmoe-1b-7b-0924-instruct": (
|
| 155 |
+
"olmoe-1b-7b-0924-instruct-q4_k_m.gguf",
|
| 156 |
+
"OLMoE-1B-7B-0924-Instruct-Q4_K_M.gguf",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
),
|
| 158 |
+
"olmo-2-1124-7b-instruct": (
|
| 159 |
+
"olmo-2-1124-7B-instruct-Q4_K_M.gguf",
|
| 160 |
+
"OLMo-2-1124-7B-Instruct-Q4_K_M.gguf",
|
| 161 |
),
|
| 162 |
}
|
| 163 |
|
distinct_mcp/external.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A catalogue of external, locally-run MCP servers, and the way in.
|
| 2 |
+
|
| 3 |
+
These are other people's programs. The in-process servers beside this module
|
| 4 |
+
run inside the worker and are governed by it; everything listed here runs as
|
| 5 |
+
a child process speaking MCP over stdio, does its own I/O, and is exactly as
|
| 6 |
+
trustworthy as its publisher. So this module is a catalogue and a doorway,
|
| 7 |
+
never an installer: nothing below downloads anything, nothing starts unless
|
| 8 |
+
an operator names the server, and a server whose credentials are absent
|
| 9 |
+
refuses at connect time with the variable names it wanted β the credential
|
| 10 |
+
values themselves are never read into this process beyond handing the child
|
| 11 |
+
its environment.
|
| 12 |
+
|
| 13 |
+
**Authentication is per server and stays in the environment.** Each entry
|
| 14 |
+
names the environment variables its server reads (an API key, or Google
|
| 15 |
+
Application Default Credentials established with ``gcloud auth login``).
|
| 16 |
+
This module checks presence, never value, and passes the environment through
|
| 17 |
+
to the child. Nothing here stores, logs or transmits a credential.
|
| 18 |
+
|
| 19 |
+
The Google entries come from https://github.com/google/mcp β the repository
|
| 20 |
+
is a directory of Google's official servers, and the ones catalogued here
|
| 21 |
+
are its "open-source, run locally" set. The Playwright and Postman entries
|
| 22 |
+
are the official publishers' packages. Remote/hosted MCP endpoints from that
|
| 23 |
+
list are deliberately not catalogued: a remote tool server is a different
|
| 24 |
+
trust conversation from a local child process, and this catalogue refuses to
|
| 25 |
+
blur it.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import os
|
| 31 |
+
import shutil
|
| 32 |
+
import subprocess
|
| 33 |
+
from collections.abc import Mapping, Sequence
|
| 34 |
+
from dataclasses import dataclass
|
| 35 |
+
|
| 36 |
+
from distinct_tools.mcp import McpError, StdioMcpTransport
|
| 37 |
+
|
| 38 |
+
__all__ = [
|
| 39 |
+
"ExternalMcpServer",
|
| 40 |
+
"EXTERNAL_CATALOGUE",
|
| 41 |
+
"connect_external",
|
| 42 |
+
"describe_external",
|
| 43 |
+
"external_by_name",
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@dataclass(frozen=True)
|
| 48 |
+
class ExternalMcpServer:
|
| 49 |
+
"""One locally-runnable server: how to start it, what it needs, who made it."""
|
| 50 |
+
|
| 51 |
+
name: str
|
| 52 |
+
publisher: str
|
| 53 |
+
summary: str
|
| 54 |
+
#: The command line, exactly. ``npx --yes`` and ``uvx`` fetch on first
|
| 55 |
+
#: run; that is the publisher's distribution mechanism, stated rather
|
| 56 |
+
#: than hidden.
|
| 57 |
+
command: tuple[str, ...]
|
| 58 |
+
#: Environment variable NAMES the server authenticates with. Presence is
|
| 59 |
+
#: checked before spawn; values are never read by this module.
|
| 60 |
+
auth_env: tuple[str, ...] = ()
|
| 61 |
+
#: Auth that is a local login state rather than a variable, e.g. Google
|
| 62 |
+
#: Application Default Credentials. Recorded for the operator's console.
|
| 63 |
+
auth_note: str = ""
|
| 64 |
+
#: Hosts the operator consents to this server reaching. A record of
|
| 65 |
+
#: consent, not a control: the child does its own I/O.
|
| 66 |
+
hosts: tuple[str, ...] = ()
|
| 67 |
+
source: str = ""
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def runtime(self) -> str:
|
| 71 |
+
return self.command[0]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
_GOOGLE_ADC = (
|
| 75 |
+
"Authenticate with Google Application Default Credentials: run "
|
| 76 |
+
"`gcloud auth application-default login` once as the operator."
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
EXTERNAL_CATALOGUE: tuple[ExternalMcpServer, ...] = (
|
| 80 |
+
ExternalMcpServer(
|
| 81 |
+
name="playwright",
|
| 82 |
+
publisher="Microsoft",
|
| 83 |
+
summary=(
|
| 84 |
+
"Drive a real browser: navigate, click, fill and read pages. Needs a "
|
| 85 |
+
"Chromium install; set PLAYWRIGHT_BROWSERS_PATH to reuse one already "
|
| 86 |
+
"on the machine instead of downloading another."
|
| 87 |
+
),
|
| 88 |
+
command=("npx", "--yes", "@playwright/mcp@latest"),
|
| 89 |
+
auth_env=(),
|
| 90 |
+
auth_note="No account. The browser itself is the capability.",
|
| 91 |
+
hosts=("*",),
|
| 92 |
+
source="https://github.com/microsoft/playwright-mcp",
|
| 93 |
+
),
|
| 94 |
+
ExternalMcpServer(
|
| 95 |
+
name="postman",
|
| 96 |
+
publisher="Postman",
|
| 97 |
+
summary=(
|
| 98 |
+
"Postman's official server: collections, environments and API "
|
| 99 |
+
"definitions in your workspaces, the public API network included."
|
| 100 |
+
),
|
| 101 |
+
command=("npx", "--yes", "@postman/postman-mcp-server"),
|
| 102 |
+
auth_env=("POSTMAN_API_KEY",),
|
| 103 |
+
auth_note="Create a key at https://postman.com/settings/me/api-keys.",
|
| 104 |
+
hosts=("api.getpostman.com",),
|
| 105 |
+
source="https://github.com/postmanlabs/postman-mcp-server",
|
| 106 |
+
),
|
| 107 |
+
ExternalMcpServer(
|
| 108 |
+
name="google-workspace",
|
| 109 |
+
publisher="Google",
|
| 110 |
+
summary="Docs, Sheets, Slides, Calendar and Gmail in the operator's Workspace.",
|
| 111 |
+
command=("npx", "--yes", "@googleworkspace/mcp"),
|
| 112 |
+
auth_env=(),
|
| 113 |
+
auth_note=_GOOGLE_ADC,
|
| 114 |
+
hosts=("*.googleapis.com",),
|
| 115 |
+
source="https://github.com/gemini-cli-extensions/workspace",
|
| 116 |
+
),
|
| 117 |
+
ExternalMcpServer(
|
| 118 |
+
name="google-analytics",
|
| 119 |
+
publisher="Google",
|
| 120 |
+
summary="Report on Analytics properties: audiences, events, conversions.",
|
| 121 |
+
command=("uvx", "analytics-mcp"),
|
| 122 |
+
auth_env=(),
|
| 123 |
+
auth_note=_GOOGLE_ADC,
|
| 124 |
+
hosts=("analyticsdata.googleapis.com", "analyticsadmin.googleapis.com"),
|
| 125 |
+
source="https://github.com/googleanalytics/google-analytics-mcp",
|
| 126 |
+
),
|
| 127 |
+
ExternalMcpServer(
|
| 128 |
+
name="google-cloud-storage",
|
| 129 |
+
publisher="Google",
|
| 130 |
+
summary="List, read and manage Cloud Storage buckets and objects.",
|
| 131 |
+
command=("npx", "--yes", "@google-cloud/storage-mcp"),
|
| 132 |
+
auth_env=(),
|
| 133 |
+
auth_note=_GOOGLE_ADC,
|
| 134 |
+
hosts=("storage.googleapis.com",),
|
| 135 |
+
source="https://github.com/googleapis/gcloud-mcp",
|
| 136 |
+
),
|
| 137 |
+
ExternalMcpServer(
|
| 138 |
+
name="gcloud",
|
| 139 |
+
publisher="Google",
|
| 140 |
+
summary="The gcloud CLI as tools: inspect and manage Google Cloud projects.",
|
| 141 |
+
command=("npx", "--yes", "@google-cloud/gcloud-mcp"),
|
| 142 |
+
auth_env=(),
|
| 143 |
+
auth_note=_GOOGLE_ADC + " Also requires the gcloud CLI on PATH.",
|
| 144 |
+
hosts=("*.googleapis.com",),
|
| 145 |
+
source="https://github.com/googleapis/gcloud-mcp",
|
| 146 |
+
),
|
| 147 |
+
ExternalMcpServer(
|
| 148 |
+
name="google-observability",
|
| 149 |
+
publisher="Google",
|
| 150 |
+
summary="Cloud Logging, Monitoring and Trace: read logs and metrics.",
|
| 151 |
+
command=("npx", "--yes", "@google-cloud/observability-mcp"),
|
| 152 |
+
auth_env=(),
|
| 153 |
+
auth_note=_GOOGLE_ADC,
|
| 154 |
+
hosts=("logging.googleapis.com", "monitoring.googleapis.com"),
|
| 155 |
+
source="https://github.com/googleapis/gcloud-mcp",
|
| 156 |
+
),
|
| 157 |
+
ExternalMcpServer(
|
| 158 |
+
name="mcp-toolbox-databases",
|
| 159 |
+
publisher="Google",
|
| 160 |
+
summary=(
|
| 161 |
+
"Databases behind one server: BigQuery, Cloud SQL, AlloyDB, Spanner, "
|
| 162 |
+
"Postgres and more, from a tools.yaml the operator writes."
|
| 163 |
+
),
|
| 164 |
+
command=("toolbox", "--stdio"),
|
| 165 |
+
auth_env=(),
|
| 166 |
+
auth_note=(
|
| 167 |
+
"Install the MCP Toolbox binary and write its tools.yaml; database "
|
| 168 |
+
"credentials live in that file or in " + _GOOGLE_ADC
|
| 169 |
+
),
|
| 170 |
+
hosts=("*",),
|
| 171 |
+
source="https://github.com/googleapis/genai-toolbox",
|
| 172 |
+
),
|
| 173 |
+
ExternalMcpServer(
|
| 174 |
+
name="gke",
|
| 175 |
+
publisher="Google",
|
| 176 |
+
summary="Kubernetes Engine: inspect clusters, workloads and events.",
|
| 177 |
+
command=("npx", "--yes", "@google-cloud/gke-mcp"),
|
| 178 |
+
auth_env=(),
|
| 179 |
+
auth_note=_GOOGLE_ADC,
|
| 180 |
+
hosts=("container.googleapis.com",),
|
| 181 |
+
source="https://github.com/GoogleCloudPlatform/gke-mcp",
|
| 182 |
+
),
|
| 183 |
+
ExternalMcpServer(
|
| 184 |
+
name="chrome-devtools",
|
| 185 |
+
publisher="Google (Chrome DevTools)",
|
| 186 |
+
summary="Debug a running Chrome: inspect pages, console, network and traces.",
|
| 187 |
+
command=("npx", "--yes", "chrome-devtools-mcp@latest"),
|
| 188 |
+
auth_env=(),
|
| 189 |
+
auth_note="No account. Attaches to a local Chrome the operator starts.",
|
| 190 |
+
hosts=(),
|
| 191 |
+
source="https://github.com/ChromeDevTools/chrome-devtools-mcp",
|
| 192 |
+
),
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def external_by_name(name: str) -> ExternalMcpServer:
|
| 197 |
+
for entry in EXTERNAL_CATALOGUE:
|
| 198 |
+
if entry.name == name:
|
| 199 |
+
return entry
|
| 200 |
+
raise McpError(f"no external MCP server named {name!r} is catalogued")
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def _missing_auth(entry: ExternalMcpServer, environ: Mapping[str, str]) -> tuple[str, ...]:
|
| 204 |
+
return tuple(var for var in entry.auth_env if not environ.get(var, "").strip())
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def describe_external(environ: Mapping[str, str] | None = None) -> str:
|
| 208 |
+
"""The operator's view: every entry, its runtime, and its auth state."""
|
| 209 |
+
|
| 210 |
+
values = os.environ if environ is None else environ
|
| 211 |
+
lines = [
|
| 212 |
+
"External MCP servers this worker can connect on request. Nothing below",
|
| 213 |
+
"is installed, running or approved until an operator names it.",
|
| 214 |
+
]
|
| 215 |
+
for entry in EXTERNAL_CATALOGUE:
|
| 216 |
+
missing = _missing_auth(entry, values)
|
| 217 |
+
runtime_present = shutil.which(entry.runtime) is not None
|
| 218 |
+
state = []
|
| 219 |
+
state.append(f"runtime {entry.runtime}: {'present' if runtime_present else 'absent'}")
|
| 220 |
+
if entry.auth_env:
|
| 221 |
+
state.append(
|
| 222 |
+
"auth: ready" if not missing else "auth: missing " + ", ".join(missing)
|
| 223 |
+
)
|
| 224 |
+
else:
|
| 225 |
+
state.append("auth: " + (entry.auth_note or "none"))
|
| 226 |
+
lines.append(f" {entry.name} ({entry.publisher})")
|
| 227 |
+
lines.append(f" {entry.summary}")
|
| 228 |
+
lines.append(f" {'; '.join(state)}")
|
| 229 |
+
return "\n".join(lines)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def connect_external(
|
| 233 |
+
name: str,
|
| 234 |
+
*,
|
| 235 |
+
environ: Mapping[str, str] | None = None,
|
| 236 |
+
timeout: float = 60.0,
|
| 237 |
+
) -> StdioMcpTransport:
|
| 238 |
+
"""Spawn one catalogued server and complete the MCP handshake.
|
| 239 |
+
|
| 240 |
+
Fails closed, with the exact variable names, when its credentials are
|
| 241 |
+
absent β a server started without them would answer the handshake and
|
| 242 |
+
then fail every real call, which is a worse failure because it is a
|
| 243 |
+
later one. The caller owns the transport and must ``close()`` it; the
|
| 244 |
+
spawn uses no shell and passes the operator's environment through
|
| 245 |
+
untouched, which is the entire credential path.
|
| 246 |
+
"""
|
| 247 |
+
|
| 248 |
+
entry = external_by_name(name)
|
| 249 |
+
values = os.environ if environ is None else environ
|
| 250 |
+
missing = _missing_auth(entry, values)
|
| 251 |
+
if missing:
|
| 252 |
+
raise McpError(
|
| 253 |
+
f"{entry.name} needs {', '.join(missing)} set in the environment. "
|
| 254 |
+
f"{entry.auth_note}".strip()
|
| 255 |
+
)
|
| 256 |
+
if shutil.which(entry.runtime) is None:
|
| 257 |
+
raise McpError(
|
| 258 |
+
f"{entry.name} needs {entry.runtime!r} on PATH; install it and retry"
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
def spawn(command: Sequence[str]) -> subprocess.Popen:
|
| 262 |
+
return subprocess.Popen(
|
| 263 |
+
list(command),
|
| 264 |
+
stdin=subprocess.PIPE,
|
| 265 |
+
stdout=subprocess.PIPE,
|
| 266 |
+
stderr=subprocess.DEVNULL,
|
| 267 |
+
env=dict(values),
|
| 268 |
+
text=True,
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
transport = StdioMcpTransport(
|
| 272 |
+
tuple(entry.command), spawn=spawn, handshake_timeout=timeout
|
| 273 |
+
)
|
| 274 |
+
transport.start()
|
| 275 |
+
return transport
|
distinct_server.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: distinct-server
|
| 3 |
+
Version: 0.1.0
|
| 4 |
+
Summary: Session-only Gradio control plane for community-run open-weight LLM agents
|
| 5 |
+
Requires-Python: >=3.10
|
| 6 |
+
Requires-Dist: gradio[oauth]==6.24.0
|
| 7 |
+
Provides-Extra: agent
|
| 8 |
+
Requires-Dist: dspy[deno]==3.3.0; extra == "agent"
|
| 9 |
+
Requires-Dist: gradio_client==2.6.0; extra == "agent"
|
| 10 |
+
Requires-Dist: pyRAPL==0.2.3.1; sys_platform == "linux" and extra == "agent"
|
| 11 |
+
Requires-Dist: textual==8.2.8; extra == "agent"
|
| 12 |
+
Provides-Extra: dev
|
| 13 |
+
Requires-Dist: pytest==8.4.2; extra == "dev"
|
| 14 |
+
Requires-Dist: ruff==0.12.11; extra == "dev"
|
distinct_server.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
README.md
|
| 2 |
+
pyproject.toml
|
| 3 |
+
distinct_agent/__init__.py
|
| 4 |
+
distinct_agent/__main__.py
|
| 5 |
+
distinct_agent/access.py
|
| 6 |
+
distinct_agent/auth.py
|
| 7 |
+
distinct_agent/cli.py
|
| 8 |
+
distinct_agent/containment.py
|
| 9 |
+
distinct_agent/dashboard.py
|
| 10 |
+
distinct_agent/deno.lock
|
| 11 |
+
distinct_agent/desktop.py
|
| 12 |
+
distinct_agent/energy.py
|
| 13 |
+
distinct_agent/fanout.py
|
| 14 |
+
distinct_agent/firstrun.py
|
| 15 |
+
distinct_agent/guard.py
|
| 16 |
+
distinct_agent/harness.py
|
| 17 |
+
distinct_agent/httpapi.py
|
| 18 |
+
distinct_agent/models.py
|
| 19 |
+
distinct_agent/queue.py
|
| 20 |
+
distinct_agent/rlm.py
|
| 21 |
+
distinct_agent/runners.py
|
| 22 |
+
distinct_agent/sandbox.py
|
| 23 |
+
distinct_agent/server_runner.py
|
| 24 |
+
distinct_agent/servers.py
|
| 25 |
+
distinct_agent/state.py
|
| 26 |
+
distinct_agent/tools.py
|
| 27 |
+
distinct_agent/transport.py
|
| 28 |
+
distinct_agent/tui.py
|
| 29 |
+
distinct_agent/weights.py
|
| 30 |
+
distinct_agent/worker.py
|
| 31 |
+
distinct_agent/isolation/__init__.py
|
| 32 |
+
distinct_agent/isolation/base.py
|
| 33 |
+
distinct_agent/isolation/linux.py
|
| 34 |
+
distinct_agent/isolation/request.py
|
| 35 |
+
distinct_agent/isolation/windows.py
|
| 36 |
+
distinct_agent/isolation/windows_appcontainer.py
|
| 37 |
+
distinct_mcp/__init__.py
|
| 38 |
+
distinct_mcp/common.py
|
| 39 |
+
distinct_mcp/files.py
|
| 40 |
+
distinct_mcp/lists.py
|
| 41 |
+
distinct_mcp/numbers.py
|
| 42 |
+
distinct_mcp/text.py
|
| 43 |
+
distinct_protocol/__init__.py
|
| 44 |
+
distinct_protocol/fences.py
|
| 45 |
+
distinct_protocol/handshake.py
|
| 46 |
+
distinct_protocol/models.py
|
| 47 |
+
distinct_protocol/netpolicy.py
|
| 48 |
+
distinct_protocol/telemetry.py
|
| 49 |
+
distinct_server/__init__.py
|
| 50 |
+
distinct_server/api.py
|
| 51 |
+
distinct_server/auth_routes.py
|
| 52 |
+
distinct_server/catalog.py
|
| 53 |
+
distinct_server/control_plane.py
|
| 54 |
+
distinct_server/identity.py
|
| 55 |
+
distinct_server/models.py
|
| 56 |
+
distinct_server/presentation.py
|
| 57 |
+
distinct_server/render.py
|
| 58 |
+
distinct_server/security.py
|
| 59 |
+
distinct_server/ui.py
|
| 60 |
+
distinct_server/ui_state.py
|
| 61 |
+
distinct_server.egg-info/PKG-INFO
|
| 62 |
+
distinct_server.egg-info/SOURCES.txt
|
| 63 |
+
distinct_server.egg-info/dependency_links.txt
|
| 64 |
+
distinct_server.egg-info/requires.txt
|
| 65 |
+
distinct_server.egg-info/top_level.txt
|
| 66 |
+
distinct_skills/NOTICE.md
|
| 67 |
+
distinct_skills/__init__.py
|
| 68 |
+
distinct_skills/__main__.py
|
| 69 |
+
distinct_skills/handlers.py
|
| 70 |
+
distinct_skills/loader.py
|
| 71 |
+
distinct_skills/manifest.py
|
| 72 |
+
distinct_skills/skills/csv-table/SKILL.md
|
| 73 |
+
distinct_skills/skills/csv-table/tool.json
|
| 74 |
+
distinct_skills/skills/review-checklist/SKILL.md
|
| 75 |
+
distinct_skills/skills/review-checklist/tool.json
|
| 76 |
+
distinct_skills/skills/review-checklist/assets/checklist.md
|
| 77 |
+
distinct_skills/skills/review-checklist/assets/claim-block.md
|
| 78 |
+
distinct_skills/skills/skill-scaffold/SKILL.md
|
| 79 |
+
distinct_skills/skills/skill-scaffold/tool.json
|
| 80 |
+
distinct_skills/skills/skill-scaffold/assets/skill-template.md
|
| 81 |
+
distinct_skills/skills/status-update/SKILL.md
|
| 82 |
+
distinct_skills/skills/status-update/tool.json
|
| 83 |
+
distinct_skills/skills/status-update/assets/status-update.md
|
| 84 |
+
distinct_tools/__init__.py
|
| 85 |
+
distinct_tools/approval.py
|
| 86 |
+
distinct_tools/core.py
|
| 87 |
+
distinct_tools/local.py
|
| 88 |
+
distinct_tools/mcp.py
|
| 89 |
+
distinct_tools/skills.py
|
| 90 |
+
distinct_tools/testing.py
|
| 91 |
+
distinct_tools/trace.py
|
| 92 |
+
distinct_tools/workspace.py
|
distinct_server.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
distinct_server.egg-info/requires.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio[oauth]==6.24.0
|
| 2 |
+
|
| 3 |
+
[agent]
|
| 4 |
+
dspy[deno]==3.3.0
|
| 5 |
+
gradio_client==2.6.0
|
| 6 |
+
textual==8.2.8
|
| 7 |
+
|
| 8 |
+
[agent:sys_platform == "linux"]
|
| 9 |
+
pyRAPL==0.2.3.1
|
| 10 |
+
|
| 11 |
+
[dev]
|
| 12 |
+
pytest==8.4.2
|
| 13 |
+
ruff==0.12.11
|
distinct_server.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
distinct_agent
|
| 2 |
+
distinct_mcp
|
| 3 |
+
distinct_protocol
|
| 4 |
+
distinct_server
|
| 5 |
+
distinct_skills
|
| 6 |
+
distinct_tools
|
distinct_server/catalog.py
CHANGED
|
@@ -20,7 +20,7 @@ never graded against each other, and never rendered in the same total.
|
|
| 20 |
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
-
from collections.abc import Iterable, Mapping
|
| 24 |
from dataclasses import dataclass
|
| 25 |
from enum import Enum
|
| 26 |
from types import MappingProxyType
|
|
@@ -137,53 +137,78 @@ def _all_missing(reason: str) -> Mapping[str, AreaResult]:
|
|
| 137 |
class ReleaseAssessment:
|
| 138 |
"""A cradle-to-release assessment of one model release.
|
| 139 |
|
| 140 |
-
``evidence_grade``
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
"""
|
| 144 |
|
| 145 |
model_id: str
|
| 146 |
coverage: str
|
| 147 |
areas: Mapping[str, AreaResult]
|
| 148 |
-
|
|
|
|
| 149 |
weights_sha256: str = ""
|
| 150 |
lineage: str = ""
|
| 151 |
boundary: str = RELEASE_BOUNDARY
|
| 152 |
reason: str = ""
|
|
|
|
| 153 |
|
| 154 |
def __post_init__(self) -> None:
|
| 155 |
missing_areas = set(RUBRIC_AREAS) - set(self.areas)
|
| 156 |
if missing_areas:
|
| 157 |
raise ValueError(f"release assessment omits areas: {sorted(missing_areas)}")
|
| 158 |
-
if self.evidence_grade is not None and self.evidence_grade not in {"A", "B", "C", "D"}:
|
| 159 |
-
raise ValueError("evidence grade must be A-D, or None for no data")
|
| 160 |
if self.coverage not in {"Complete", "Partial", "Insufficient", "No data"}:
|
| 161 |
raise ValueError("coverage must be Complete, Partial, Insufficient or No data")
|
| 162 |
-
if self.coverage == "No data" and self.
|
| 163 |
-
raise ValueError("'No data'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
object.__setattr__(self, "areas", MappingProxyType(dict(self.areas)))
|
|
|
|
| 165 |
|
| 166 |
@property
|
| 167 |
def has_any_result(self) -> bool:
|
| 168 |
return any(item.status is not AreaStatus.MISSING for item in self.areas.values())
|
| 169 |
|
| 170 |
@property
|
| 171 |
-
def
|
| 172 |
-
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
def to_dict(self) -> dict:
|
| 178 |
return {
|
| 179 |
"model_id": self.model_id,
|
| 180 |
-
"evidence_grade": self.evidence_grade,
|
| 181 |
-
"grade_display": self.grade_display,
|
| 182 |
"coverage": self.coverage,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
"boundary": self.boundary,
|
| 184 |
"weights_sha256": self.weights_sha256,
|
| 185 |
"lineage": self.lineage,
|
| 186 |
"reason": self.reason,
|
|
|
|
| 187 |
"areas": {area: item.to_dict() for area, item in self.areas.items()},
|
| 188 |
}
|
| 189 |
|
|
@@ -191,143 +216,262 @@ class ReleaseAssessment:
|
|
| 191 |
# LABELS NAME THE PUBLICATION, NOT THE SIZE.
|
| 192 |
#
|
| 193 |
# These used to read "smallest", "balanced test model" and "largest test
|
| 194 |
-
# model". Size is not something this
|
| 195 |
-
# name of the thing being chosen invited exactly the reasoning
|
| 196 |
# exists to refuse: that a smaller file is a better environmental choice. It
|
| 197 |
# might be, and it might not, and the only way to know is a published result.
|
| 198 |
# What belongs in a label is who published the weights, because that is what
|
| 199 |
-
# the
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
MODEL_CATALOG: dict[str, ModelManifest] = {
|
| 201 |
-
# DEFAULT of the worker catalogue:
|
| 202 |
-
#
|
| 203 |
-
#
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
| 209 |
context_length=4_096,
|
| 210 |
),
|
| 211 |
-
"
|
| 212 |
-
id="
|
| 213 |
-
label="
|
| 214 |
-
filename="
|
| 215 |
min_ram_gb=8.0,
|
| 216 |
context_length=4_096,
|
| 217 |
),
|
| 218 |
-
"qwen3-0.6b": ModelManifest(
|
| 219 |
-
id="qwen3-0.6b",
|
| 220 |
-
label="Qwen3 0.6B Β· unsloth",
|
| 221 |
-
filename="Qwen3-0.6B-Q4_K_M.gguf",
|
| 222 |
-
min_ram_gb=2,
|
| 223 |
-
context_length=40_960,
|
| 224 |
-
),
|
| 225 |
-
"qwen3-4b": ModelManifest(
|
| 226 |
-
id="qwen3-4b",
|
| 227 |
-
label="Qwen3 4B Β· unsloth",
|
| 228 |
-
filename="Qwen3-4B-Q4_K_M.gguf",
|
| 229 |
-
min_ram_gb=6,
|
| 230 |
-
context_length=40_960,
|
| 231 |
-
),
|
| 232 |
"llama-2-7b-chat": ModelManifest(
|
| 233 |
id="llama-2-7b-chat",
|
| 234 |
-
label="Llama 2 7B Chat Β· gated licence",
|
| 235 |
filename="llama-2-7b-chat.Q4_K_M.gguf",
|
| 236 |
-
min_ram_gb=8,
|
| 237 |
context_length=4_096,
|
| 238 |
),
|
| 239 |
-
"gpt-oss-20b": ModelManifest(
|
| 240 |
-
id="gpt-oss-20b",
|
| 241 |
-
label="gpt-oss-20b Β· MXFP4",
|
| 242 |
-
filename="gpt-oss-20b-mxfp4.gguf",
|
| 243 |
-
min_ram_gb=16,
|
| 244 |
-
context_length=131_072,
|
| 245 |
-
),
|
| 246 |
}
|
| 247 |
|
| 248 |
|
| 249 |
-
#
|
| 250 |
#
|
| 251 |
-
# The
|
| 252 |
-
# tokens, parameters, TDP, PUE, grid factors
|
| 253 |
-
#
|
| 254 |
-
#
|
| 255 |
-
#
|
| 256 |
-
# "disclosed scale, hardware, location and controls" has been deleted, because
|
| 257 |
# it was caught reasoning "a small 0.49B checkpoint moderates demand" into a
|
| 258 |
-
# grade
|
| 259 |
-
# instead of a number. An area now either has a published result or it is
|
| 260 |
-
# Missing.
|
| 261 |
#
|
| 262 |
-
#
|
| 263 |
-
#
|
| 264 |
-
#
|
| 265 |
-
#
|
| 266 |
-
#
|
| 267 |
-
#
|
|
|
|
| 268 |
#
|
| 269 |
-
#
|
| 270 |
-
#
|
| 271 |
-
#
|
| 272 |
-
#
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
RELEASE_ASSESSMENT: dict[str, ReleaseAssessment] = {
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
coverage="No data",
|
| 283 |
-
areas=_all_missing(
|
| 284 |
-
"Rendered from model_assessments.json where a published result exists; "
|
| 285 |
-
"this in-catalogue record deliberately carries none."
|
| 286 |
),
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
"
|
| 294 |
-
"this in-catalogue record deliberately carries none."
|
| 295 |
),
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
),
|
| 305 |
-
reason="no published model-specific assessment for this release",
|
| 306 |
),
|
| 307 |
-
"
|
| 308 |
-
model_id="
|
| 309 |
-
coverage="
|
| 310 |
-
|
| 311 |
-
"
|
| 312 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
),
|
| 314 |
-
reason="no published model-specific assessment for this release",
|
| 315 |
),
|
| 316 |
"llama-2-7b-chat": ReleaseAssessment(
|
| 317 |
model_id="llama-2-7b-chat",
|
| 318 |
-
coverage="
|
| 319 |
-
|
| 320 |
-
"
|
|
|
|
| 321 |
),
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
),
|
| 330 |
-
reason="no published model-specific assessment recorded here",
|
| 331 |
),
|
| 332 |
}
|
| 333 |
|
|
|
|
| 20 |
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
+
from collections.abc import Iterable, Mapping, Sequence
|
| 24 |
from dataclasses import dataclass
|
| 25 |
from enum import Enum
|
| 26 |
from types import MappingProxyType
|
|
|
|
| 137 |
class ReleaseAssessment:
|
| 138 |
"""A cradle-to-release assessment of one model release.
|
| 139 |
|
| 140 |
+
This used to carry an ``evidence_grade`` letter alongside the areas, and a
|
| 141 |
+
sibling ordinal for impact. Both are gone. A letter is a ranking this
|
| 142 |
+
catalogue invented on top of numbers somebody else published, and once the
|
| 143 |
+
published numbers are shown in full -- with their units, their method and
|
| 144 |
+
their primary source -- the letter adds no information and one more way to
|
| 145 |
+
be wrong. What replaced it is ``coverage``, which is not a score: it counts
|
| 146 |
+
how many of the six areas have a published result and says nothing about
|
| 147 |
+
whether those results are large or small.
|
| 148 |
"""
|
| 149 |
|
| 150 |
model_id: str
|
| 151 |
coverage: str
|
| 152 |
areas: Mapping[str, AreaResult]
|
| 153 |
+
summary: str = ""
|
| 154 |
+
covers: str = ""
|
| 155 |
weights_sha256: str = ""
|
| 156 |
lineage: str = ""
|
| 157 |
boundary: str = RELEASE_BOUNDARY
|
| 158 |
reason: str = ""
|
| 159 |
+
links: Sequence[Mapping[str, str]] = ()
|
| 160 |
|
| 161 |
def __post_init__(self) -> None:
|
| 162 |
missing_areas = set(RUBRIC_AREAS) - set(self.areas)
|
| 163 |
if missing_areas:
|
| 164 |
raise ValueError(f"release assessment omits areas: {sorted(missing_areas)}")
|
|
|
|
|
|
|
| 165 |
if self.coverage not in {"Complete", "Partial", "Insufficient", "No data"}:
|
| 166 |
raise ValueError("coverage must be Complete, Partial, Insufficient or No data")
|
| 167 |
+
if self.coverage == "No data" and self.has_any_result:
|
| 168 |
+
raise ValueError("'No data' cannot carry a published result")
|
| 169 |
+
if self.coverage != "No data" and not self.has_any_result:
|
| 170 |
+
raise ValueError("a coverage other than 'No data' requires at least one result")
|
| 171 |
+
for link in self.links:
|
| 172 |
+
if not link.get("label") or not link.get("url"):
|
| 173 |
+
raise ValueError("every link needs both a label and a url")
|
| 174 |
object.__setattr__(self, "areas", MappingProxyType(dict(self.areas)))
|
| 175 |
+
object.__setattr__(self, "links", tuple(MappingProxyType(dict(link)) for link in self.links))
|
| 176 |
|
| 177 |
@property
|
| 178 |
def has_any_result(self) -> bool:
|
| 179 |
return any(item.status is not AreaStatus.MISSING for item in self.areas.values())
|
| 180 |
|
| 181 |
@property
|
| 182 |
+
def assessed_areas(self) -> int:
|
| 183 |
+
return sum(1 for item in self.areas.values() if item.status is not AreaStatus.MISSING)
|
| 184 |
|
| 185 |
+
@property
|
| 186 |
+
def coverage_display(self) -> str:
|
| 187 |
+
"""Coverage and its boundary, inseparable.
|
| 188 |
+
|
| 189 |
+
Coverage on its own invites the reading that four of six is a middling
|
| 190 |
+
score. It is not a score at all, so the count always travels with the
|
| 191 |
+
word that says what it counts.
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
if self.coverage == "No data":
|
| 195 |
+
return "no published result"
|
| 196 |
+
return f"{self.coverage.lower()} Β· {self.assessed_areas} of {len(RUBRIC_AREAS)} areas published"
|
| 197 |
|
| 198 |
def to_dict(self) -> dict:
|
| 199 |
return {
|
| 200 |
"model_id": self.model_id,
|
|
|
|
|
|
|
| 201 |
"coverage": self.coverage,
|
| 202 |
+
"coverage_display": self.coverage_display,
|
| 203 |
+
"assessed_areas": self.assessed_areas,
|
| 204 |
+
"total_areas": len(RUBRIC_AREAS),
|
| 205 |
+
"summary": self.summary,
|
| 206 |
+
"covers": self.covers,
|
| 207 |
"boundary": self.boundary,
|
| 208 |
"weights_sha256": self.weights_sha256,
|
| 209 |
"lineage": self.lineage,
|
| 210 |
"reason": self.reason,
|
| 211 |
+
"links": [dict(link) for link in self.links],
|
| 212 |
"areas": {area: item.to_dict() for area, item in self.areas.items()},
|
| 213 |
}
|
| 214 |
|
|
|
|
| 216 |
# LABELS NAME THE PUBLICATION, NOT THE SIZE.
|
| 217 |
#
|
| 218 |
# These used to read "smallest", "balanced test model" and "largest test
|
| 219 |
+
# model". Size is not something this catalogue evaluates, and putting it in the
|
| 220 |
+
# name of the thing being chosen invited exactly the reasoning this surface
|
| 221 |
# exists to refuse: that a smaller file is a better environmental choice. It
|
| 222 |
# might be, and it might not, and the only way to know is a published result.
|
| 223 |
# What belongs in a label is who published the weights, because that is what
|
| 224 |
+
# the reader has to be able to check.
|
| 225 |
+
#
|
| 226 |
+
# WHY THESE THREE. Every model here has a published, model-specific assessment
|
| 227 |
+
# of its training. Models with none were removed rather than listed with an
|
| 228 |
+
# empty record; see the note in ``distinct_agent.models`` for what went and why.
|
| 229 |
MODEL_CATALOG: dict[str, ModelManifest] = {
|
| 230 |
+
# DEFAULT of the worker catalogue: the lowest published figures of any
|
| 231 |
+
# assessed release at a size this network can run. Kept in step with
|
| 232 |
+
# distinct_agent.models.KNOWN_MODEL_MANIFESTS; the drift test in
|
| 233 |
+
# tests/test_catalog.py pins filename, RAM and context length.
|
| 234 |
+
"olmoe-1b-7b-0924-instruct": ModelManifest(
|
| 235 |
+
id="olmoe-1b-7b-0924-instruct",
|
| 236 |
+
label="OLMoE 1B-7B 0924 Instruct Β· Ai2 first-party GGUF",
|
| 237 |
+
filename="olmoe-1b-7b-0924-instruct-q4_k_m.gguf",
|
| 238 |
+
min_ram_gb=8.0,
|
| 239 |
context_length=4_096,
|
| 240 |
),
|
| 241 |
+
"olmo-2-1124-7b-instruct": ModelManifest(
|
| 242 |
+
id="olmo-2-1124-7b-instruct",
|
| 243 |
+
label="OLMo 2 1124 7B Instruct Β· Ai2 first-party GGUF",
|
| 244 |
+
filename="olmo-2-1124-7B-instruct-Q4_K_M.gguf",
|
| 245 |
min_ram_gb=8.0,
|
| 246 |
context_length=4_096,
|
| 247 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
"llama-2-7b-chat": ModelManifest(
|
| 249 |
id="llama-2-7b-chat",
|
| 250 |
+
label="Llama 2 7B Chat Β· Meta Β· gated licence",
|
| 251 |
filename="llama-2-7b-chat.Q4_K_M.gguf",
|
| 252 |
+
min_ram_gb=8.0,
|
| 253 |
context_length=4_096,
|
| 254 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
}
|
| 256 |
|
| 257 |
|
| 258 |
+
# THE ONE ROUTE, AND WHAT IT NOW LETS THROUGH.
|
| 259 |
#
|
| 260 |
+
# The rule has not changed: a published, model-specific result, used exactly as
|
| 261 |
+
# published. GPU-hours, FLOPs, tokens, parameters, TDP, PUE, grid factors and
|
| 262 |
+
# another model's assessment must not be converted here into invented MWh,
|
| 263 |
+
# tonnes or litres. The contextual route that once allowed an ordinal judgement
|
| 264 |
+
# from "disclosed scale, hardware, location and controls" stays deleted, because
|
|
|
|
| 265 |
# it was caught reasoning "a small 0.49B checkpoint moderates demand" into a
|
| 266 |
+
# grade, which is the parameters-to-impact conversion wearing a letter.
|
|
|
|
|
|
|
| 267 |
#
|
| 268 |
+
# What changed is that the catalogue now holds models this rule has something to
|
| 269 |
+
# say about. Every entry below carries real published figures instead of the
|
| 270 |
+
# uniform "No data" this structure used to render, and the detail -- the method
|
| 271 |
+
# behind each number, the primary source, the plain-language summary -- lives in
|
| 272 |
+
# ``model_assessments.json`` and is rendered from there by ``distinct_server.ui``.
|
| 273 |
+
# This structure is the in-catalogue record: enough to be correct on its own if
|
| 274 |
+
# that file is absent, never the fuller surface.
|
| 275 |
#
|
| 276 |
+
# WHAT IS STILL FORBIDDEN. Llama 2's energy area stays Missing even though Meta
|
| 277 |
+
# disclosed 184,320 GPU-hours at 400 W, because GPU-hours are not joules and the
|
| 278 |
+
# multiplication is ours, not Meta's. Naming the disclosure in the note is the
|
| 279 |
+
# honest handling; converting it is not.
|
| 280 |
+
_AI2_ASSESSMENT = "https://arxiv.org/abs/2503.05804"
|
| 281 |
+
_LLAMA2_ASSESSMENT = "https://arxiv.org/abs/2307.09288"
|
| 282 |
+
|
| 283 |
+
_NO_LAND = AreaResult(
|
| 284 |
+
AreaStatus.MISSING,
|
| 285 |
+
note=(
|
| 286 |
+
"No published per-model land or biodiversity figure exists for any model, from "
|
| 287 |
+
"any lab. ITU-T L.1801 lists the area and states the methodology is developing."
|
| 288 |
+
),
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
RELEASE_ASSESSMENT: dict[str, ReleaseAssessment] = {
|
| 292 |
+
"olmoe-1b-7b-0924-instruct": ReleaseAssessment(
|
| 293 |
+
model_id="olmoe-1b-7b-0924-instruct",
|
| 294 |
+
coverage="Partial",
|
| 295 |
+
covers=(
|
| 296 |
+
"The 0924 pretraining run. The tuning that produced this Instruct checkpoint "
|
| 297 |
+
"is not separately costed in the source."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
),
|
| 299 |
+
summary=(
|
| 300 |
+
"The lowest published footprint of any assessed release at a size this network "
|
| 301 |
+
"can run. Ai2 sampled real GPU power at sub-second intervals rather than "
|
| 302 |
+
"assuming chips draw their rated wattage, which makes the energy figure one of "
|
| 303 |
+
"very few genuinely measured training numbers in print. Carbon and water follow "
|
| 304 |
+
"from that measured energy using the site's own grid factor and cooling "
|
| 305 |
+
"efficiency, so they describe Texas as much as they describe the model."
|
|
|
|
| 306 |
),
|
| 307 |
+
areas={
|
| 308 |
+
"energy": AreaResult(
|
| 309 |
+
AreaStatus.MEASURED,
|
| 310 |
+
value="54",
|
| 311 |
+
unit="MWh",
|
| 312 |
+
note="Final pretraining run, Jupiter cluster, Texas.",
|
| 313 |
+
source_url=_AI2_ASSESSMENT,
|
| 314 |
+
),
|
| 315 |
+
"climate": AreaResult(
|
| 316 |
+
AreaStatus.MODELLED,
|
| 317 |
+
value="18",
|
| 318 |
+
unit="tCO2e",
|
| 319 |
+
note=(
|
| 320 |
+
"Measured energy at 0.332 kg CO2 per kWh, PUE 1.2. Location-based; "
|
| 321 |
+
"no offset applied."
|
| 322 |
+
),
|
| 323 |
+
source_url=_AI2_ASSESSMENT,
|
| 324 |
+
),
|
| 325 |
+
"water": AreaResult(
|
| 326 |
+
AreaStatus.MODELLED,
|
| 327 |
+
value="70",
|
| 328 |
+
unit="kL",
|
| 329 |
+
note=(
|
| 330 |
+
"Measured energy at a water usage effectiveness of 1.29 L per kWh. "
|
| 331 |
+
"On-site cooling only; upstream power-station water is not counted."
|
| 332 |
+
),
|
| 333 |
+
source_url=_AI2_ASSESSMENT,
|
| 334 |
+
),
|
| 335 |
+
"land": _NO_LAND,
|
| 336 |
+
"materials": AreaResult(
|
| 337 |
+
AreaStatus.MISSING,
|
| 338 |
+
note=(
|
| 339 |
+
"Ai2 report 22 tCO2e and 4.8 kL of embodied hardware impact across their "
|
| 340 |
+
"whole programme. That is a programme total, not this release's share."
|
| 341 |
+
),
|
| 342 |
+
),
|
| 343 |
+
"pollution": AreaResult(AreaStatus.MISSING, note="Not assessed in the source."),
|
| 344 |
+
},
|
| 345 |
+
links=(
|
| 346 |
+
{
|
| 347 |
+
"label": "Model card",
|
| 348 |
+
"url": "https://huggingface.co/allenai/OLMoE-1B-7B-0924-Instruct",
|
| 349 |
+
},
|
| 350 |
+
{
|
| 351 |
+
"label": "Pinned weights",
|
| 352 |
+
"url": "https://huggingface.co/allenai/OLMoE-1B-7B-0924-Instruct-GGUF",
|
| 353 |
+
},
|
| 354 |
+
{"label": "Published assessment", "url": _AI2_ASSESSMENT},
|
| 355 |
),
|
|
|
|
| 356 |
),
|
| 357 |
+
"olmo-2-1124-7b-instruct": ReleaseAssessment(
|
| 358 |
+
model_id="olmo-2-1124-7b-instruct",
|
| 359 |
+
coverage="Partial",
|
| 360 |
+
covers=(
|
| 361 |
+
"The 1124 pretraining run. The tuning that produced this Instruct checkpoint "
|
| 362 |
+
"is not separately costed in the source."
|
| 363 |
+
),
|
| 364 |
+
summary=(
|
| 365 |
+
"A dense 7B measured by the same team, in the same data centre, in the same "
|
| 366 |
+
"year, by the same method as OLMoE. That makes the pair one of the few honest "
|
| 367 |
+
"like-for-like comparisons in this field: about three times the energy, carbon "
|
| 368 |
+
"and water for comparable capability. Offered as the fallback if the "
|
| 369 |
+
"mixture-of-experts architecture gives trouble locally, not because it is lighter."
|
| 370 |
+
),
|
| 371 |
+
areas={
|
| 372 |
+
"energy": AreaResult(
|
| 373 |
+
AreaStatus.MEASURED,
|
| 374 |
+
value="157",
|
| 375 |
+
unit="MWh",
|
| 376 |
+
note="Final pretraining run, Jupiter cluster, Texas.",
|
| 377 |
+
source_url=_AI2_ASSESSMENT,
|
| 378 |
+
),
|
| 379 |
+
"climate": AreaResult(
|
| 380 |
+
AreaStatus.MODELLED,
|
| 381 |
+
value="52",
|
| 382 |
+
unit="tCO2e",
|
| 383 |
+
note=(
|
| 384 |
+
"Measured energy at 0.332 kg CO2 per kWh, PUE 1.2. Location-based; "
|
| 385 |
+
"no offset applied."
|
| 386 |
+
),
|
| 387 |
+
source_url=_AI2_ASSESSMENT,
|
| 388 |
+
),
|
| 389 |
+
"water": AreaResult(
|
| 390 |
+
AreaStatus.MODELLED,
|
| 391 |
+
value="202",
|
| 392 |
+
unit="kL",
|
| 393 |
+
note=(
|
| 394 |
+
"Measured energy at 1.29 L per kWh. The 13B sibling drank four times as "
|
| 395 |
+
"much on the same architecture because it trained in Iowa, where the "
|
| 396 |
+
"figure is 3.1 L per kWh: water tracks the site more than the model."
|
| 397 |
+
),
|
| 398 |
+
source_url=_AI2_ASSESSMENT,
|
| 399 |
+
),
|
| 400 |
+
"land": _NO_LAND,
|
| 401 |
+
"materials": AreaResult(
|
| 402 |
+
AreaStatus.MISSING,
|
| 403 |
+
note=(
|
| 404 |
+
"Ai2 report 22 tCO2e and 4.8 kL of embodied hardware impact across their "
|
| 405 |
+
"whole programme. That is a programme total, not this release's share."
|
| 406 |
+
),
|
| 407 |
+
),
|
| 408 |
+
"pollution": AreaResult(AreaStatus.MISSING, note="Not assessed in the source."),
|
| 409 |
+
},
|
| 410 |
+
links=(
|
| 411 |
+
{
|
| 412 |
+
"label": "Model card",
|
| 413 |
+
"url": "https://huggingface.co/allenai/OLMo-2-1124-7B-Instruct",
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
"label": "Pinned weights",
|
| 417 |
+
"url": "https://huggingface.co/allenai/OLMo-2-1124-7B-Instruct-GGUF",
|
| 418 |
+
},
|
| 419 |
+
{"label": "Published assessment", "url": _AI2_ASSESSMENT},
|
| 420 |
),
|
|
|
|
| 421 |
),
|
| 422 |
"llama-2-7b-chat": ReleaseAssessment(
|
| 423 |
model_id="llama-2-7b-chat",
|
| 424 |
+
coverage="Partial",
|
| 425 |
+
covers=(
|
| 426 |
+
"Llama 2 7B pretraining. This Chat checkpoint is a post-trained variant; Meta "
|
| 427 |
+
"report fine-tuning compute inside the family total without breaking it out."
|
| 428 |
),
|
| 429 |
+
summary=(
|
| 430 |
+
"The only non-Ai2 release here with a published model-specific figure, and a "
|
| 431 |
+
"thin one: carbon and nothing else, modelled from rated chip wattage rather "
|
| 432 |
+
"than measured draw. It is a different kind of number from the two above and "
|
| 433 |
+
"should not be lined up beside them. Meta also report a second carbon column "
|
| 434 |
+
"reading zero on the grounds that they buy matching renewable electricity "
|
| 435 |
+
"annually; that is an accounting position, not a physical one, and the figure "
|
| 436 |
+
"shown here is what the grid actually delivered."
|
| 437 |
+
),
|
| 438 |
+
reason="unpinned: Meta publish no GGUF and the source repository is gated",
|
| 439 |
+
areas={
|
| 440 |
+
"energy": AreaResult(
|
| 441 |
+
AreaStatus.MISSING,
|
| 442 |
+
note=(
|
| 443 |
+
"Meta disclosed 184,320 A100-80GB GPU-hours at 400 W rated draw but "
|
| 444 |
+
"published no energy figure. Multiplying those out would be an estimate "
|
| 445 |
+
"this catalogue invented, so the area stays Missing and the disclosure "
|
| 446 |
+
"is named instead."
|
| 447 |
+
),
|
| 448 |
+
),
|
| 449 |
+
"climate": AreaResult(
|
| 450 |
+
AreaStatus.MODELLED,
|
| 451 |
+
value="31.22",
|
| 452 |
+
unit="tCO2e",
|
| 453 |
+
note=(
|
| 454 |
+
"Modelled by Meta from GPU-hours and rated device power. Location-based. "
|
| 455 |
+
"Meta separately report it as fully offset, which nets to zero on paper "
|
| 456 |
+
"but not on the grid."
|
| 457 |
+
),
|
| 458 |
+
source_url=_LLAMA2_ASSESSMENT,
|
| 459 |
+
),
|
| 460 |
+
"water": AreaResult(AreaStatus.MISSING, note="Not disclosed."),
|
| 461 |
+
"land": _NO_LAND,
|
| 462 |
+
"materials": AreaResult(
|
| 463 |
+
AreaStatus.MISSING,
|
| 464 |
+
note="Not disclosed. Embodied hardware impact is absent from Meta's accounting.",
|
| 465 |
+
),
|
| 466 |
+
"pollution": AreaResult(AreaStatus.MISSING, note="Not disclosed."),
|
| 467 |
+
},
|
| 468 |
+
links=(
|
| 469 |
+
{
|
| 470 |
+
"label": "Model card",
|
| 471 |
+
"url": "https://huggingface.co/meta-llama/Llama-2-7b-chat-hf",
|
| 472 |
+
},
|
| 473 |
+
{"label": "Published assessment", "url": _LLAMA2_ASSESSMENT},
|
| 474 |
),
|
|
|
|
| 475 |
),
|
| 476 |
}
|
| 477 |
|
distinct_server/presentation.py
CHANGED
|
@@ -84,6 +84,40 @@ HEAD = """
|
|
| 84 |
href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..800&display=swap">
|
| 85 |
<link rel="stylesheet"
|
| 86 |
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400..700&display=swap">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
"""
|
| 88 |
|
| 89 |
|
|
@@ -1320,4 +1354,93 @@ details[open] .c-libhelp__toggle::after{content:" -";}
|
|
| 1320 |
.c-panel code,.c-panel pre{overflow-wrap:anywhere;}
|
| 1321 |
.c-join__qr svg{width:128px; height:128px;}
|
| 1322 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1323 |
"""
|
|
|
|
| 84 |
href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..800&display=swap">
|
| 85 |
<link rel="stylesheet"
|
| 86 |
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400..700&display=swap">
|
| 87 |
+
|
| 88 |
+
<script>
|
| 89 |
+
(function(){
|
| 90 |
+
function media(el){return el.closest ? el.closest('.c-libcard__media') : null;}
|
| 91 |
+
function show(m){
|
| 92 |
+
if(!m || m.classList.contains('is-playing')) return;
|
| 93 |
+
var src = m.getAttribute('data-gif');
|
| 94 |
+
if(!src) return;
|
| 95 |
+
var img = document.createElement('img');
|
| 96 |
+
img.src = src; img.alt='';
|
| 97 |
+
m.appendChild(img); m.classList.add('is-playing');
|
| 98 |
+
}
|
| 99 |
+
function hide(m){
|
| 100 |
+
if(!m) return;
|
| 101 |
+
m.classList.remove('is-playing');
|
| 102 |
+
m.querySelectorAll('img').forEach(function(i){i.remove();});
|
| 103 |
+
}
|
| 104 |
+
document.addEventListener('mouseover', function(e){show(media(e.target));});
|
| 105 |
+
document.addEventListener('mouseout', function(e){
|
| 106 |
+
var m = media(e.target);
|
| 107 |
+
if(m && !m.contains(e.relatedTarget)) hide(m);
|
| 108 |
+
});
|
| 109 |
+
var holdTimer=null;
|
| 110 |
+
document.addEventListener('touchstart', function(e){
|
| 111 |
+
var m = media(e.target); if(!m) return;
|
| 112 |
+
holdTimer = setTimeout(function(){show(m);}, 300);
|
| 113 |
+
}, {passive:true});
|
| 114 |
+
['touchend','touchcancel'].forEach(function(name){
|
| 115 |
+
document.addEventListener(name, function(e){
|
| 116 |
+
clearTimeout(holdTimer); hide(media(e.target));
|
| 117 |
+
}, {passive:true});
|
| 118 |
+
});
|
| 119 |
+
})();
|
| 120 |
+
</script>
|
| 121 |
"""
|
| 122 |
|
| 123 |
|
|
|
|
| 1354 |
.c-panel code,.c-panel pre{overflow-wrap:anywhere;}
|
| 1355 |
.c-join__qr svg{width:128px; height:128px;}
|
| 1356 |
}
|
| 1357 |
+
|
| 1358 |
+
|
| 1359 |
+
/* ---- Library page: cards over tick boxes ------------------------------ */
|
| 1360 |
+
.c-library-section{scroll-margin-top:24px;}
|
| 1361 |
+
.c-libgrid{display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr));
|
| 1362 |
+
gap:var(--s4); margin:var(--s4) 0;}
|
| 1363 |
+
.c-libcard{background:var(--c-surface); border:1px solid var(--c-hairline);
|
| 1364 |
+
border-radius:var(--r-lg); padding:var(--s4);
|
| 1365 |
+
display:flex; flex-direction:column; gap:6px;
|
| 1366 |
+
transition:box-shadow .18s ease, transform .18s ease;}
|
| 1367 |
+
.c-libcard:hover{box-shadow:var(--e2); transform:translateY(-2px);}
|
| 1368 |
+
.c-libcard__media{margin:calc(var(--s4)*-1) calc(var(--s4)*-1) var(--s2);
|
| 1369 |
+
aspect-ratio:16/9; border-radius:var(--r-lg) var(--r-lg) 0 0;
|
| 1370 |
+
background:var(--c-fill) center/cover no-repeat;
|
| 1371 |
+
display:flex; align-items:center; justify-content:center;
|
| 1372 |
+
overflow:hidden; position:relative;}
|
| 1373 |
+
.c-libcard__media img{width:100%; height:100%; object-fit:cover; display:block;}
|
| 1374 |
+
.c-libcard__play{font-size:22px; color:var(--c-muted); opacity:.8;}
|
| 1375 |
+
.c-libcard__media.is-playing .c-libcard__play{display:none;}
|
| 1376 |
+
.c-libcard__head{display:flex; align-items:center; gap:var(--s2);}
|
| 1377 |
+
.c-libcard__kind{font-size:var(--t-micro); letter-spacing:.06em; text-transform:uppercase;
|
| 1378 |
+
padding:2px 8px; border-radius:999px; background:var(--c-fill);
|
| 1379 |
+
color:var(--c-text-2);}
|
| 1380 |
+
.c-libcard__kind--skill{background:var(--c-sage-wash, #e7efe7); color:var(--c-sage-deep);}
|
| 1381 |
+
.c-libcard__default{margin-left:auto; font-size:var(--t-micro); color:var(--c-sage-deep);
|
| 1382 |
+
border:1px solid currentColor; border-radius:999px; padding:1px 8px;}
|
| 1383 |
+
.c-libcard__name{margin:0; font-size:var(--t-body); font-weight:650; color:var(--c-text);
|
| 1384 |
+
text-transform:capitalize;}
|
| 1385 |
+
.c-libcard__author{margin:0; font-size:var(--t-micro); color:var(--c-muted);}
|
| 1386 |
+
.c-libcard__desc{margin:2px 0 0; font-size:var(--t-small, 13px); line-height:1.5;
|
| 1387 |
+
color:var(--c-text-2);
|
| 1388 |
+
display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical;
|
| 1389 |
+
overflow:hidden;}
|
| 1390 |
+
.c-libcard__ref{margin-top:auto; padding-top:var(--s2); font-family:var(--mono);
|
| 1391 |
+
font-size:11px; color:var(--c-muted);}
|
| 1392 |
+
.c-librarydefaults-head{margin-top:var(--s4);}
|
| 1393 |
+
|
| 1394 |
+
/* ---- Quick nav under the identity box --------------------------------- */
|
| 1395 |
+
.c-quicknav{display:flex; gap:var(--s3); justify-content:flex-end;
|
| 1396 |
+
margin:calc(var(--s2)*-1) 0 var(--s2);}
|
| 1397 |
+
.c-quicknav__link{font-size:var(--t-micro); letter-spacing:.04em; text-decoration:none;
|
| 1398 |
+
color:var(--c-sage-deep); border:1px solid var(--c-hairline);
|
| 1399 |
+
background:var(--c-surface); padding:4px 12px; border-radius:999px;}
|
| 1400 |
+
.c-quicknav__link:hover{border-color:var(--c-sage-deep);}
|
| 1401 |
+
|
| 1402 |
+
/* ---- The no-agents veil ----------------------------------------------- */
|
| 1403 |
+
/* Shown only while somebody is signed in (the identity span has text) and
|
| 1404 |
+
the worker panel is in its empty state. Pure CSS state: no events. */
|
| 1405 |
+
.c-veil{display:none;}
|
| 1406 |
+
.c-conversation{position:relative;}
|
| 1407 |
+
.c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-veil{
|
| 1408 |
+
display:flex; position:absolute; inset:0; z-index:12;
|
| 1409 |
+
align-items:center; justify-content:center; padding:var(--s5);
|
| 1410 |
+
background:color-mix(in srgb, var(--c-bg, #f7f9f3) 62%, transparent);
|
| 1411 |
+
backdrop-filter:blur(2.5px); border-radius:var(--r-lg);
|
| 1412 |
+
/* The veil informs; it does not imprison. Pointer events pass through
|
| 1413 |
+
everywhere except its own panel, so a transient no-worker blip (a poll
|
| 1414 |
+
gap, a mode change) can never trap a click mid-flow. */
|
| 1415 |
+
pointer-events:none;}
|
| 1416 |
+
.c-veil__panel{pointer-events:auto;}
|
| 1417 |
+
.c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-conversation > *:not(.c-veil):not(:first-child){
|
| 1418 |
+
opacity:.45; filter:saturate(.7);}
|
| 1419 |
+
.c-veil__panel{max-width:380px; text-align:center; background:var(--c-surface);
|
| 1420 |
+
border:1px solid var(--c-hairline); border-radius:var(--r-lg);
|
| 1421 |
+
box-shadow:var(--e2); padding:var(--s5);}
|
| 1422 |
+
.c-veil__title{margin:0 0 6px; font-weight:650; font-size:var(--t-lg); color:var(--c-text);}
|
| 1423 |
+
.c-veil__body{margin:0 0 var(--s4); color:var(--c-text-2); line-height:1.55;}
|
| 1424 |
+
.c-veil__action{display:inline-block; text-decoration:none; background:var(--c-sage-deep);
|
| 1425 |
+
color:#fff; padding:10px 20px; border-radius:999px; font-weight:600;}
|
| 1426 |
+
.c-veil__action:hover{filter:brightness(1.08);}
|
| 1427 |
+
|
| 1428 |
+
/* ---- Professional polish pass ----------------------------------------- */
|
| 1429 |
+
/* Calmer sections: the tail accordions read as pages, not widgets. */
|
| 1430 |
+
.c-tail .gradio-accordion, .c-tail details{background:var(--c-surface);
|
| 1431 |
+
border:1px solid var(--c-hairline) !important; border-radius:var(--r-lg) !important;
|
| 1432 |
+
padding:var(--s3) var(--s4) !important; margin-bottom:var(--s4) !important;}
|
| 1433 |
+
.c-tail .label-wrap span{font-weight:650 !important; font-size:var(--t-lg) !important;}
|
| 1434 |
+
/* One shadow language everywhere. */
|
| 1435 |
+
.c-grade-card,.c-panel,.c-legend,.c-workers article{box-shadow:none !important;}
|
| 1436 |
+
/* Inputs: quiet until focused. */
|
| 1437 |
+
.gradio-container input[type="text"], .gradio-container textarea{
|
| 1438 |
+
border-radius:var(--r-md) !important;}
|
| 1439 |
+
@media (max-width:700px){
|
| 1440 |
+
.c-libgrid{grid-template-columns:1fr 1fr; gap:var(--s3);}
|
| 1441 |
+
.c-quicknav{justify-content:flex-start;}
|
| 1442 |
+
}
|
| 1443 |
+
@media (max-width:460px){
|
| 1444 |
+
.c-libgrid{grid-template-columns:1fr;}
|
| 1445 |
+
}
|
| 1446 |
"""
|
distinct_server/render.py
CHANGED
|
@@ -177,7 +177,7 @@ def no_tools_note(notice_text: str) -> str:
|
|
| 177 |
)
|
| 178 |
|
| 179 |
|
| 180 |
-
def join_this_server(*, addresses, code: str, qr: str = "") -> str:
|
| 181 |
"""What to point an agent at, with the address filled in.
|
| 182 |
|
| 183 |
This panel exists because the previous instruction was a command with
|
|
@@ -215,11 +215,13 @@ def join_this_server(*, addresses, code: str, qr: str = "") -> str:
|
|
| 215 |
else '<p class="c-join__lead">Generate a pairing code below, then run the '
|
| 216 |
"command with it.</p>"
|
| 217 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
command = (
|
| 219 |
-
f"python -m distinct_agent
|
| 220 |
-
f"
|
| 221 |
-
f" --pair {code or 'YOUR-CODE'} \\\n"
|
| 222 |
-
f" --models qwen3-0.6b --tools local"
|
| 223 |
)
|
| 224 |
picture = f'<div class="c-join__qr">{qr}</div>' if qr else ""
|
| 225 |
return (
|
|
@@ -509,7 +511,7 @@ def worker_cards(rows: Sequence[Sequence[object]], *, narrowed_by: str = "") ->
|
|
| 509 |
"""
|
| 510 |
|
| 511 |
if not rows:
|
| 512 |
-
return empty_state(
|
| 513 |
glyph="β",
|
| 514 |
title="No compatible worker",
|
| 515 |
# Which of the choices above emptied the list. Without this the
|
|
@@ -531,7 +533,7 @@ def worker_cards(rows: Sequence[Sequence[object]], *, narrowed_by: str = "") ->
|
|
| 531 |
)
|
| 532 |
),
|
| 533 |
compact=True,
|
| 534 |
-
)
|
| 535 |
|
| 536 |
cards: list[str] = []
|
| 537 |
for row in rows:
|
|
@@ -781,6 +783,18 @@ def energy_headline(
|
|
| 781 |
)
|
| 782 |
|
| 783 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
def format_joules(joules: float) -> str:
|
| 785 |
"""A figure with a unit people read without converting.
|
| 786 |
|
|
@@ -1162,6 +1176,79 @@ def _energy_block(lines: Sequence[str], *, measured: bool, quiet: bool = False)
|
|
| 1162 |
# --------------------------------------------------------------------------
|
| 1163 |
|
| 1164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1165 |
# --------------------------------------------------------------------------
|
| 1166 |
# Better Images of AI: the artwork set, and attribution that cannot drift
|
| 1167 |
# --------------------------------------------------------------------------
|
|
|
|
| 177 |
)
|
| 178 |
|
| 179 |
|
| 180 |
+
def join_this_server(*, addresses, code: str, qr: str = "", tools: str = "local") -> str:
|
| 181 |
"""What to point an agent at, with the address filled in.
|
| 182 |
|
| 183 |
This panel exists because the previous instruction was a command with
|
|
|
|
| 215 |
else '<p class="c-join__lead">Generate a pairing code below, then run the '
|
| 216 |
"command with it.</p>"
|
| 217 |
)
|
| 218 |
+
# One line on purpose. The previous form used backslash continuations,
|
| 219 |
+
# which are bash syntax: pasted into PowerShell they became stray
|
| 220 |
+
# arguments and the command half-ran. A single line pastes correctly
|
| 221 |
+
# into every shell anybody actually uses.
|
| 222 |
command = (
|
| 223 |
+
f"python -m distinct_agent --server {first} "
|
| 224 |
+
f"--pair {code or 'YOUR-CODE'} --models qwen3-0.6b --tools {tools or 'local'}"
|
|
|
|
|
|
|
| 225 |
)
|
| 226 |
picture = f'<div class="c-join__qr">{qr}</div>' if qr else ""
|
| 227 |
return (
|
|
|
|
| 511 |
"""
|
| 512 |
|
| 513 |
if not rows:
|
| 514 |
+
return '<div class="c-noworker-hook">' + empty_state(
|
| 515 |
glyph="β",
|
| 516 |
title="No compatible worker",
|
| 517 |
# Which of the choices above emptied the list. Without this the
|
|
|
|
| 533 |
)
|
| 534 |
),
|
| 535 |
compact=True,
|
| 536 |
+
) + "</div>"
|
| 537 |
|
| 538 |
cards: list[str] = []
|
| 539 |
for row in rows:
|
|
|
|
| 783 |
)
|
| 784 |
|
| 785 |
|
| 786 |
+
def format_joules_per_token(value: float) -> str:
|
| 787 |
+
"""Joules per token at a scale people can read.
|
| 788 |
+
|
| 789 |
+
A small model's figure is millijoules; a big machine's is joules. Two
|
| 790 |
+
decades of unit, chosen by size, so neither reads as zero.
|
| 791 |
+
"""
|
| 792 |
+
|
| 793 |
+
if value >= 1.0:
|
| 794 |
+
return f"{value:.2f} J/tok"
|
| 795 |
+
return f"{value * 1000:.1f} mJ/tok"
|
| 796 |
+
|
| 797 |
+
|
| 798 |
def format_joules(joules: float) -> str:
|
| 799 |
"""A figure with a unit people read without converting.
|
| 800 |
|
|
|
|
| 1176 |
# --------------------------------------------------------------------------
|
| 1177 |
|
| 1178 |
|
| 1179 |
+
|
| 1180 |
+
# --------------------------------------------------------------------------
|
| 1181 |
+
# Library cards
|
| 1182 |
+
# --------------------------------------------------------------------------
|
| 1183 |
+
|
| 1184 |
+
_DEMO_DIR = (Path(__file__).resolve().parent.parent / "assets" / "library-demos")
|
| 1185 |
+
_DEMO_BASE = f"/gradio_api/file={_DEMO_DIR.as_posix()}"
|
| 1186 |
+
|
| 1187 |
+
|
| 1188 |
+
def _demo_gif(ref: str) -> str:
|
| 1189 |
+
"""The demo GIF's URL for a library member, or nothing.
|
| 1190 |
+
|
| 1191 |
+
The file is looked for on disk at render time so a card never promises a
|
| 1192 |
+
demonstration the repository does not carry. Slug: the ref with ``@`` and
|
| 1193 |
+
``.`` flattened, which is how the recorder names them.
|
| 1194 |
+
"""
|
| 1195 |
+
|
| 1196 |
+
slug = ref.replace("@", "-").replace(".", "-")
|
| 1197 |
+
if (_DEMO_DIR / f"{slug}.gif").is_file():
|
| 1198 |
+
return f"{_DEMO_BASE}/{slug}.gif"
|
| 1199 |
+
return ""
|
| 1200 |
+
|
| 1201 |
+
|
| 1202 |
+
def library_cards(rows: Sequence[Mapping[str, object]], chosen: Sequence[str]) -> str:
|
| 1203 |
+
"""The library as cards a person reads, not a list of tick boxes.
|
| 1204 |
+
|
| 1205 |
+
Every field is escaped: descriptions come from tool specs, which workers
|
| 1206 |
+
and third-party repositories can influence. The demo image is attached as
|
| 1207 |
+
``data-gif`` and only swapped in by the page's own hover script, so a
|
| 1208 |
+
card with no recording is simply a card, not a broken image.
|
| 1209 |
+
"""
|
| 1210 |
+
|
| 1211 |
+
if not rows:
|
| 1212 |
+
return empty_state(
|
| 1213 |
+
glyph="β",
|
| 1214 |
+
title="Nothing matches",
|
| 1215 |
+
body="No library member matches that search. Clear it to see the shelf.",
|
| 1216 |
+
)
|
| 1217 |
+
picked = set(chosen)
|
| 1218 |
+
cards = []
|
| 1219 |
+
for row in rows:
|
| 1220 |
+
ref = str(row.get("ref", ""))
|
| 1221 |
+
kind = str(row.get("kind", "tool"))
|
| 1222 |
+
name = str(row.get("name", ref))
|
| 1223 |
+
author = str(row.get("author", "distinct"))
|
| 1224 |
+
description = str(row.get("description", ""))
|
| 1225 |
+
gif = _demo_gif(ref)
|
| 1226 |
+
media = (
|
| 1227 |
+
f'<figure class="c-libcard__media" data-gif="{esc(gif)}">'
|
| 1228 |
+
'<span class="c-libcard__play" aria-hidden="true">▸</span>'
|
| 1229 |
+
"<figcaption class=\"c-visually-hidden\">Hover or hold to preview</figcaption>"
|
| 1230 |
+
"</figure>"
|
| 1231 |
+
if gif
|
| 1232 |
+
else ""
|
| 1233 |
+
)
|
| 1234 |
+
default_mark = (
|
| 1235 |
+
'<span class="c-libcard__default">default</span>' if ref in picked else ""
|
| 1236 |
+
)
|
| 1237 |
+
cards.append(
|
| 1238 |
+
f'<article class="c-libcard c-libcard--{esc(kind)}">'
|
| 1239 |
+
f"{media}"
|
| 1240 |
+
'<div class="c-libcard__head">'
|
| 1241 |
+
f'<span class="c-libcard__kind c-libcard__kind--{esc(kind)}">{esc(kind)}</span>'
|
| 1242 |
+
f"{default_mark}"
|
| 1243 |
+
"</div>"
|
| 1244 |
+
f'<h3 class="c-libcard__name">{esc(name)}</h3>'
|
| 1245 |
+
f'<p class="c-libcard__author">by {esc(author)}</p>'
|
| 1246 |
+
f'<p class="c-libcard__desc">{esc(description)}</p>'
|
| 1247 |
+
f'<code class="c-libcard__ref">{esc(ref)}</code>'
|
| 1248 |
+
"</article>"
|
| 1249 |
+
)
|
| 1250 |
+
return '<div class="c-libgrid">' + "".join(cards) + "</div>"
|
| 1251 |
+
|
| 1252 |
# --------------------------------------------------------------------------
|
| 1253 |
# Better Images of AI: the artwork set, and attribution that cannot drift
|
| 1254 |
# --------------------------------------------------------------------------
|
distinct_server/ui.py
CHANGED
|
@@ -13,9 +13,10 @@ from typing import Any, Mapping, Optional, Sequence
|
|
| 13 |
import gradio as gr
|
| 14 |
|
| 15 |
from distinct_protocol import (
|
| 16 |
-
AGENT_MODES,
|
| 17 |
MAX_LIVE_STEP_TEXT,
|
| 18 |
MAX_RUN_STEPS,
|
|
|
|
|
|
|
| 19 |
PHASE_CALLING_TOOL,
|
| 20 |
PHASE_FETCHING_WEIGHTS,
|
| 21 |
PHASE_GENERATING,
|
|
@@ -27,8 +28,6 @@ from distinct_protocol import (
|
|
| 27 |
PHASE_QUEUED_ON_WORKER,
|
| 28 |
PHASE_WAITING_FOR_WORKER,
|
| 29 |
PHASE_WORKING,
|
| 30 |
-
MODE_AGENT,
|
| 31 |
-
MODE_SIMPLE,
|
| 32 |
STEP_KINDS,
|
| 33 |
STEP_PHASE,
|
| 34 |
AgentStatus,
|
|
@@ -37,13 +36,13 @@ from distinct_protocol import (
|
|
| 37 |
ToolSelection,
|
| 38 |
normalise_phase,
|
| 39 |
)
|
| 40 |
-
from distinct_protocol.fences import FILE_FENCE_BODIES
|
| 41 |
from distinct_protocol.fences import defang as _defang_fences
|
| 42 |
-
from distinct_protocol.fences import defang_marker_value
|
| 43 |
from distinct_protocol.handshake import Allowance
|
| 44 |
|
| 45 |
from . import render
|
| 46 |
from .api import AgentApi
|
|
|
|
| 47 |
from .catalog import (
|
| 48 |
AREA_LABELS,
|
| 49 |
MODEL_CATALOG,
|
|
@@ -54,7 +53,6 @@ from .catalog import (
|
|
| 54 |
model_choices,
|
| 55 |
)
|
| 56 |
from .control_plane import ControlPlane
|
| 57 |
-
from .auth_routes import LOGIN_PATH, identity_from_session
|
| 58 |
from .identity import (
|
| 59 |
LoginRequired,
|
| 60 |
SessionAuthorizer,
|
|
@@ -186,15 +184,24 @@ def _library_specs() -> Mapping[str, Mapping[str, str]]:
|
|
| 186 |
|
| 187 |
try:
|
| 188 |
import distinct_tools
|
|
|
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
"kind": getattr(spec, "kind", "tool"),
|
| 193 |
"name": str(spec.tool_id).replace("_", " "),
|
| 194 |
"description": str(getattr(spec, "description", "")),
|
|
|
|
| 195 |
}
|
| 196 |
-
|
| 197 |
-
}
|
| 198 |
except Exception:
|
| 199 |
return {}
|
| 200 |
|
|
@@ -401,11 +408,29 @@ class EnergyReading:
|
|
| 401 |
scope: str
|
| 402 |
provider: str
|
| 403 |
reason: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
@property
|
| 406 |
def measured(self) -> bool:
|
| 407 |
return self.status == ENERGY_MEASURED
|
| 408 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
@property
|
| 410 |
def line(self) -> str:
|
| 411 |
"""The one line a completed run carries in the transcript.
|
|
@@ -427,7 +452,16 @@ class EnergyReading:
|
|
| 427 |
return f"Electricity: β **No data**{detail} Β· not counted as zero"
|
| 428 |
means = render.comparator(float(self.joules or 0.0))
|
| 429 |
suffix = f" Β· {means}" if means else ""
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
|
| 432 |
|
| 433 |
def _energy_reading(result: Any) -> EnergyReading:
|
|
@@ -470,7 +504,13 @@ def _energy_reading(result: Any) -> EnergyReading:
|
|
| 470 |
return EnergyReading(
|
| 471 |
ENERGY_MISSING, None, scope, provider, reason or "worker reported no joule value"
|
| 472 |
)
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
|
| 476 |
def _session_energy_markdown(readings: Sequence[EnergyReading]) -> str:
|
|
@@ -683,8 +723,27 @@ def _measurement_note(readings: Sequence[EnergyReading]) -> str:
|
|
| 683 |
if providers:
|
| 684 |
parts.append("Measured by: " + ", ".join(providers) + ".")
|
| 685 |
parts.append(
|
| 686 |
-
"
|
| 687 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
)
|
| 689 |
return " ".join(parts)
|
| 690 |
|
|
@@ -1057,7 +1116,6 @@ def _grade_suffix(model_id: str) -> str:
|
|
| 1057 |
return "no assessment recorded"
|
| 1058 |
overall = record.get("overall") if isinstance(record.get("overall"), Mapping) else {}
|
| 1059 |
evidence = record.get("evidence") if isinstance(record.get("evidence"), Mapping) else {}
|
| 1060 |
-
parts: list[str] = []
|
| 1061 |
letter = _letter(overall.get("letter"))
|
| 1062 |
if rules.get("impact") and str(overall.get("state") or "") == "grade" and letter:
|
| 1063 |
assessed = overall.get("areas_assessed")
|
|
@@ -1195,10 +1253,15 @@ def _default_server_catalogue() -> "Allowance":
|
|
| 1195 |
tools: tuple[str, ...] = ()
|
| 1196 |
try:
|
| 1197 |
import distinct_tools
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1202 |
except Exception:
|
| 1203 |
tools = ()
|
| 1204 |
return Allowance(models=tuple(MODEL_CATALOG), tools=tools)
|
|
@@ -1283,19 +1346,27 @@ class DistinctUI:
|
|
| 1283 |
|
| 1284 |
# Library ----------------------------------------------------------
|
| 1285 |
|
| 1286 |
-
def _advertised_library_refs(self) -> tuple[str, ...]:
|
| 1287 |
-
"""Every exact ``id@version``
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1288 |
|
| 1289 |
refs: set[str] = set()
|
| 1290 |
try:
|
| 1291 |
-
for view in self.control.list_agents():
|
| 1292 |
for item in view.capabilities.tools:
|
| 1293 |
refs.add(item if "@" in item else f"{item}@1.0.0")
|
| 1294 |
except ControlPlaneError:
|
| 1295 |
pass
|
| 1296 |
return tuple(sorted(refs))
|
| 1297 |
|
| 1298 |
-
def _library_choices(self) -> list[tuple[str, str]]:
|
| 1299 |
"""Options for the composer's library picker, named for people.
|
| 1300 |
|
| 1301 |
The label leads with a readable name and its kind, because that is
|
|
@@ -1306,7 +1377,7 @@ class DistinctUI:
|
|
| 1306 |
|
| 1307 |
specs = _library_specs()
|
| 1308 |
choices: list[tuple[str, str]] = []
|
| 1309 |
-
for ref in self._advertised_library_refs():
|
| 1310 |
spec = specs.get(ref)
|
| 1311 |
if spec is None:
|
| 1312 |
choices.append((f"{ref} Β· library member", ref))
|
|
@@ -1333,8 +1404,12 @@ class DistinctUI:
|
|
| 1333 |
)
|
| 1334 |
return render.library_help(rows)
|
| 1335 |
|
| 1336 |
-
def _tool_selections(
|
| 1337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1338 |
result = []
|
| 1339 |
for value in values or ():
|
| 1340 |
if not isinstance(value, str) or "@" not in value:
|
|
@@ -1353,6 +1428,7 @@ class DistinctUI:
|
|
| 1353 |
selected: Optional[Sequence[str]] = None,
|
| 1354 |
*,
|
| 1355 |
defaults: Optional[Sequence[str]] = None,
|
|
|
|
| 1356 |
) -> tuple[Any, str]:
|
| 1357 |
"""The refreshed selector plus its undernote, preserving selection.
|
| 1358 |
|
|
@@ -1365,7 +1441,7 @@ class DistinctUI:
|
|
| 1365 |
what a run starts with, never what it is stuck with.
|
| 1366 |
"""
|
| 1367 |
|
| 1368 |
-
choices = self._library_choices()
|
| 1369 |
valid = {ref for _, ref in choices}
|
| 1370 |
if selected is None and defaults:
|
| 1371 |
value = [ref for ref in defaults if ref in valid]
|
|
@@ -1409,8 +1485,10 @@ class DistinctUI:
|
|
| 1409 |
rows.append({"ref": ref, **spec})
|
| 1410 |
return rows[: self.LIBRARY_PAGE_LIMIT], len(rows)
|
| 1411 |
|
| 1412 |
-
def _library_page(
|
| 1413 |
-
|
|
|
|
|
|
|
| 1414 |
|
| 1415 |
rows, matched = self._library_rows(query)
|
| 1416 |
total = len(_library_specs())
|
|
@@ -1429,11 +1507,12 @@ class DistinctUI:
|
|
| 1429 |
render.library_summary(
|
| 1430 |
shown=len(rows), matched=matched, total=total, chosen=len(kept)
|
| 1431 |
),
|
|
|
|
| 1432 |
)
|
| 1433 |
|
| 1434 |
def update_library_page(
|
| 1435 |
self, query: str, state: Mapping[str, Any]
|
| 1436 |
-
) -> tuple[Any, str]:
|
| 1437 |
"""Re-filter the page without touching what has been chosen."""
|
| 1438 |
|
| 1439 |
return self._library_page(query, _library_defaults(state))
|
|
@@ -1443,7 +1522,9 @@ class DistinctUI:
|
|
| 1443 |
visible_choice: Sequence[str],
|
| 1444 |
query: str,
|
| 1445 |
state: Mapping[str, Any],
|
| 1446 |
-
|
|
|
|
|
|
|
| 1447 |
"""Record the defaults, merging the visible page into the kept set.
|
| 1448 |
|
| 1449 |
The subtlety is that the component only reports what it can see. A
|
|
@@ -1462,11 +1543,17 @@ class DistinctUI:
|
|
| 1462 |
}
|
| 1463 |
specs = _library_specs()
|
| 1464 |
updated = {**state, "library_defaults": sorted(ref for ref in chosen if ref in specs)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1465 |
selector, note = self._library_components(
|
| 1466 |
-
_library_defaults(updated),
|
|
|
|
|
|
|
| 1467 |
)
|
| 1468 |
-
_, summary = self._library_page(query, _library_defaults(updated))
|
| 1469 |
-
return updated, updated, summary, selector, note
|
| 1470 |
|
| 1471 |
# Outputs ----------------------------------------------------------
|
| 1472 |
|
|
@@ -1777,9 +1864,9 @@ class DistinctUI:
|
|
| 1777 |
DEFAULT_MODEL_ID, (), "auto", MODE_SIMPLE, viewer_id=user_id
|
| 1778 |
)
|
| 1779 |
library, library_note = self._library_components(
|
| 1780 |
-
None, defaults=_library_defaults(updated)
|
| 1781 |
)
|
| 1782 |
-
page, page_summary = self._library_page("", _library_defaults(updated))
|
| 1783 |
return (
|
| 1784 |
updated,
|
| 1785 |
updated,
|
|
@@ -1800,6 +1887,7 @@ class DistinctUI:
|
|
| 1800 |
# the moment the accordion is expanded.
|
| 1801 |
page,
|
| 1802 |
page_summary,
|
|
|
|
| 1803 |
)
|
| 1804 |
|
| 1805 |
def _live_steps(self, view: Any) -> list[Mapping[str, Any]]:
|
|
@@ -1893,7 +1981,7 @@ class DistinctUI:
|
|
| 1893 |
"""
|
| 1894 |
|
| 1895 |
try:
|
| 1896 |
-
tools = self._tool_selections(tool_values)
|
| 1897 |
views = self.control.compatible_agents(
|
| 1898 |
model_id, tools, viewer_id=viewer_id, include_offline=True
|
| 1899 |
)
|
|
@@ -2181,7 +2269,7 @@ class DistinctUI:
|
|
| 2181 |
raise ValueError("Enter a request")
|
| 2182 |
if model_id not in MODEL_CATALOG:
|
| 2183 |
raise ValueError("Select a supported model")
|
| 2184 |
-
tools = self._tool_selections(tool_values)
|
| 2185 |
conversation = active_conversation(state)
|
| 2186 |
parent_job_id = conversation["job_ids"][-1] if conversation["job_ids"] else None
|
| 2187 |
stored = self._store_attachments(state["session_id"], uploads or ())
|
|
@@ -2274,11 +2362,17 @@ class DistinctUI:
|
|
| 2274 |
# CheckboxGroup on every tick races the user's own clicks: a box
|
| 2275 |
# ticked while a tick was in flight would be silently unticked by the
|
| 2276 |
# stale value coming back.
|
| 2277 |
-
current_refs = self._advertised_library_refs()
|
| 2278 |
-
|
| 2279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2280 |
library, library_note = self._library_components(
|
| 2281 |
-
tool_values or None,
|
|
|
|
|
|
|
| 2282 |
)
|
| 2283 |
else:
|
| 2284 |
library, library_note = gr.skip(), gr.skip()
|
|
@@ -2878,7 +2972,7 @@ class DistinctUI:
|
|
| 2878 |
pass
|
| 2879 |
return ""
|
| 2880 |
|
| 2881 |
-
def _join_panel(self, code: str = "") -> str:
|
| 2882 |
"""The address an agent should be pointed at, and the code if minted.
|
| 2883 |
|
| 2884 |
The port comes from the same policy the launch uses, so the panel
|
|
@@ -2909,7 +3003,11 @@ class DistinctUI:
|
|
| 2909 |
qr = qr_svg(addresses[0])
|
| 2910 |
except Exception: # noqa: BLE001 - the address alone is enough
|
| 2911 |
qr = ""
|
| 2912 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2913 |
|
| 2914 |
# Pairing/downloads ------------------------------------------------
|
| 2915 |
|
|
@@ -2922,7 +3020,7 @@ class DistinctUI:
|
|
| 2922 |
try:
|
| 2923 |
user_id = self._authorize(state, profile, request)
|
| 2924 |
value = self.control.create_pairing_code(user_id)
|
| 2925 |
-
return self._join_panel(value.code)
|
| 2926 |
except (ControlPlaneError, LoginRequired, ValueError) as exc:
|
| 2927 |
raise gr.Error(str(exc)) from exc
|
| 2928 |
|
|
@@ -3011,6 +3109,15 @@ class DistinctUI:
|
|
| 3011 |
# button below is the one sign-in control, so a header link
|
| 3012 |
# would only duplicate it.
|
| 3013 |
identity = gr.HTML(_identity_html(""))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3014 |
|
| 3015 |
# THE SIGN-IN CONTROL, AND WHY IT IS CONDITIONAL.
|
| 3016 |
#
|
|
@@ -3177,6 +3284,23 @@ class DistinctUI:
|
|
| 3177 |
)
|
| 3178 |
|
| 3179 |
with gr.Column(elem_classes="c-conversation", min_width=0):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3180 |
chat = gr.Chatbot(
|
| 3181 |
label="Conversation",
|
| 3182 |
show_label=False,
|
|
@@ -3297,7 +3421,9 @@ class DistinctUI:
|
|
| 3297 |
# left edge of their own. Inside the tail they share the shell's
|
| 3298 |
# padding box like everything else.
|
| 3299 |
with gr.Column(elem_classes="c-tail"):
|
| 3300 |
-
with gr.Accordion(
|
|
|
|
|
|
|
| 3301 |
gr.Markdown(
|
| 3302 |
# "The agent owns its bounded FIFO and reports it
|
| 3303 |
# on every poll" is true and is a sentence for
|
|
@@ -3327,7 +3453,7 @@ class DistinctUI:
|
|
| 3327 |
"Download agent", variant="secondary"
|
| 3328 |
)
|
| 3329 |
create_code = gr.Button("Generate pairing code")
|
| 3330 |
-
|
| 3331 |
|
| 3332 |
# THE LIBRARY, AT THE SIZE IT IS GOING TO BE.
|
| 3333 |
#
|
|
@@ -3338,12 +3464,17 @@ class DistinctUI:
|
|
| 3338 |
# person does here is choose their defaults once; those
|
| 3339 |
# are remembered in this browser and tick themselves on
|
| 3340 |
# every run where a worker offers them.
|
| 3341 |
-
with gr.Accordion(
|
|
|
|
|
|
|
|
|
|
| 3342 |
gr.Markdown(
|
| 3343 |
-
"
|
| 3344 |
-
"
|
| 3345 |
-
"
|
| 3346 |
-
"
|
|
|
|
|
|
|
| 3347 |
)
|
| 3348 |
library_search = gr.Textbox(
|
| 3349 |
label="Search the library",
|
|
@@ -3355,6 +3486,8 @@ class DistinctUI:
|
|
| 3355 |
library_summary = gr.HTML(
|
| 3356 |
render.library_summary(shown=0, matched=0, total=0, chosen=0)
|
| 3357 |
)
|
|
|
|
|
|
|
| 3358 |
library_defaults = gr.CheckboxGroup(
|
| 3359 |
label="Your defaults",
|
| 3360 |
show_label=False,
|
|
@@ -3412,6 +3545,7 @@ class DistinctUI:
|
|
| 3412 |
activity_panel,
|
| 3413 |
library_defaults,
|
| 3414 |
library_summary,
|
|
|
|
| 3415 |
],
|
| 3416 |
queue=False,
|
| 3417 |
api_visibility="private",
|
|
@@ -3612,7 +3746,7 @@ class DistinctUI:
|
|
| 3612 |
library_search.input(
|
| 3613 |
self.update_library_page,
|
| 3614 |
inputs=[library_search, session_state],
|
| 3615 |
-
outputs=[library_defaults, library_summary],
|
| 3616 |
queue=False,
|
| 3617 |
api_visibility="private",
|
| 3618 |
)
|
|
@@ -3623,6 +3757,7 @@ class DistinctUI:
|
|
| 3623 |
session_state,
|
| 3624 |
browser_state,
|
| 3625 |
library_summary,
|
|
|
|
| 3626 |
tools,
|
| 3627 |
library_note,
|
| 3628 |
],
|
|
|
|
| 13 |
import gradio as gr
|
| 14 |
|
| 15 |
from distinct_protocol import (
|
|
|
|
| 16 |
MAX_LIVE_STEP_TEXT,
|
| 17 |
MAX_RUN_STEPS,
|
| 18 |
+
MODE_AGENT,
|
| 19 |
+
MODE_SIMPLE,
|
| 20 |
PHASE_CALLING_TOOL,
|
| 21 |
PHASE_FETCHING_WEIGHTS,
|
| 22 |
PHASE_GENERATING,
|
|
|
|
| 28 |
PHASE_QUEUED_ON_WORKER,
|
| 29 |
PHASE_WAITING_FOR_WORKER,
|
| 30 |
PHASE_WORKING,
|
|
|
|
|
|
|
| 31 |
STEP_KINDS,
|
| 32 |
STEP_PHASE,
|
| 33 |
AgentStatus,
|
|
|
|
| 36 |
ToolSelection,
|
| 37 |
normalise_phase,
|
| 38 |
)
|
| 39 |
+
from distinct_protocol.fences import FILE_FENCE_BODIES, defang_marker_value
|
| 40 |
from distinct_protocol.fences import defang as _defang_fences
|
|
|
|
| 41 |
from distinct_protocol.handshake import Allowance
|
| 42 |
|
| 43 |
from . import render
|
| 44 |
from .api import AgentApi
|
| 45 |
+
from .auth_routes import identity_from_session
|
| 46 |
from .catalog import (
|
| 47 |
AREA_LABELS,
|
| 48 |
MODEL_CATALOG,
|
|
|
|
| 53 |
model_choices,
|
| 54 |
)
|
| 55 |
from .control_plane import ControlPlane
|
|
|
|
| 56 |
from .identity import (
|
| 57 |
LoginRequired,
|
| 58 |
SessionAuthorizer,
|
|
|
|
| 184 |
|
| 185 |
try:
|
| 186 |
import distinct_tools
|
| 187 |
+
from distinct_tools.skills import REPOSITORY_SKILLS, REPOSITORY_SPECS
|
| 188 |
|
| 189 |
+
adapted: dict[str, str] = {}
|
| 190 |
+
for skill in REPOSITORY_SKILLS:
|
| 191 |
+
source = str(getattr(skill, "source", "") or "")
|
| 192 |
+
adapted[skill.tool_id] = (
|
| 193 |
+
"Anthropic Β· adapted" if "anthropics/skills" in source else "distinct"
|
| 194 |
+
)
|
| 195 |
+
specs: dict[str, dict[str, str]] = {}
|
| 196 |
+
for spec in tuple(distinct_tools.INSTALLABLE_SPECS) + tuple(REPOSITORY_SPECS):
|
| 197 |
+
ref = f"{spec.tool_id}@{spec.version}"
|
| 198 |
+
specs[ref] = {
|
| 199 |
"kind": getattr(spec, "kind", "tool"),
|
| 200 |
"name": str(spec.tool_id).replace("_", " "),
|
| 201 |
"description": str(getattr(spec, "description", "")),
|
| 202 |
+
"author": adapted.get(str(spec.tool_id), "distinct"),
|
| 203 |
}
|
| 204 |
+
return specs
|
|
|
|
| 205 |
except Exception:
|
| 206 |
return {}
|
| 207 |
|
|
|
|
| 408 |
scope: str
|
| 409 |
provider: str
|
| 410 |
reason: Optional[str] = None
|
| 411 |
+
#: Real output token count from the runner, when the runner counted one.
|
| 412 |
+
#: Never derived from characters: an estimated token is not a token.
|
| 413 |
+
output_tokens: Optional[int] = None
|
| 414 |
|
| 415 |
@property
|
| 416 |
def measured(self) -> bool:
|
| 417 |
return self.status == ENERGY_MEASURED
|
| 418 |
|
| 419 |
+
@property
|
| 420 |
+
def joules_per_token(self) -> Optional[float]:
|
| 421 |
+
"""Joules per generated token: the efficiency figure, when honest.
|
| 422 |
+
|
| 423 |
+
Defined only when both halves were actually measured β a joule figure
|
| 424 |
+
from the meter and a token count from the runner. This is the
|
| 425 |
+
``average power Γ· generation speed`` identity, computed on totals.
|
| 426 |
+
"""
|
| 427 |
+
|
| 428 |
+
if not self.measured or not self.joules or not self.output_tokens:
|
| 429 |
+
return None
|
| 430 |
+
if self.output_tokens <= 0:
|
| 431 |
+
return None
|
| 432 |
+
return float(self.joules) / float(self.output_tokens)
|
| 433 |
+
|
| 434 |
@property
|
| 435 |
def line(self) -> str:
|
| 436 |
"""The one line a completed run carries in the transcript.
|
|
|
|
| 452 |
return f"Electricity: β **No data**{detail} Β· not counted as zero"
|
| 453 |
means = render.comparator(float(self.joules or 0.0))
|
| 454 |
suffix = f" Β· {means}" if means else ""
|
| 455 |
+
per_token = ""
|
| 456 |
+
if self.joules_per_token is not None:
|
| 457 |
+
per_token = (
|
| 458 |
+
f" Β· {self.output_tokens} tok"
|
| 459 |
+
f" Β· {render.format_joules_per_token(self.joules_per_token)}"
|
| 460 |
+
)
|
| 461 |
+
return (
|
| 462 |
+
f"Electricity: **{render.format_joules(float(self.joules))}**"
|
| 463 |
+
f"{per_token}{suffix}"
|
| 464 |
+
)
|
| 465 |
|
| 466 |
|
| 467 |
def _energy_reading(result: Any) -> EnergyReading:
|
|
|
|
| 504 |
return EnergyReading(
|
| 505 |
ENERGY_MISSING, None, scope, provider, reason or "worker reported no joule value"
|
| 506 |
)
|
| 507 |
+
tokens = None
|
| 508 |
+
usage = payload.get("usage")
|
| 509 |
+
if isinstance(usage, Mapping):
|
| 510 |
+
raw_tokens = usage.get("output_tokens")
|
| 511 |
+
if isinstance(raw_tokens, int) and not isinstance(raw_tokens, bool) and raw_tokens > 0:
|
| 512 |
+
tokens = raw_tokens
|
| 513 |
+
return EnergyReading(ENERGY_MEASURED, total, scope, provider, output_tokens=tokens)
|
| 514 |
|
| 515 |
|
| 516 |
def _session_energy_markdown(readings: Sequence[EnergyReading]) -> str:
|
|
|
|
| 723 |
if providers:
|
| 724 |
parts.append("Measured by: " + ", ".join(providers) + ".")
|
| 725 |
parts.append(
|
| 726 |
+
"Measured figures come from hardware counters β Intel RAPL through "
|
| 727 |
+
"powercap or the Windows EMI driver, an NVIDIA board through NVML or "
|
| 728 |
+
"nvidia-smi sampling, Apple silicon through its package counters β "
|
| 729 |
+
"and modelled figures come from CPU busy-time and say so in their "
|
| 730 |
+
"scope. The two kinds are never added together: different scopes are "
|
| 731 |
+
"different questions."
|
| 732 |
+
)
|
| 733 |
+
parts.append(
|
| 734 |
+
"Where the runner reported a real token count, each answer also shows "
|
| 735 |
+
"joules per generated token β total energy over total tokens, the "
|
| 736 |
+
"same identity as average power divided by generation speed. Token "
|
| 737 |
+
"counts are never estimated from characters."
|
| 738 |
+
)
|
| 739 |
+
parts.append(
|
| 740 |
+
"Agent-mode runs spend energy beyond generation: tool calls and the "
|
| 741 |
+
"safety screen are separate passes, and the guard's cost is recorded "
|
| 742 |
+
"against the run apart from the answer's own β the pipeline overhead "
|
| 743 |
+
"that measurement studies of agentic systems (e.g. arXiv:2604.00053) "
|
| 744 |
+
"find can dominate. Reported by the worker that ran it and not "
|
| 745 |
+
"verifiable from here. A run that returned no measurement is counted "
|
| 746 |
+
"as a run and never as zero."
|
| 747 |
)
|
| 748 |
return " ".join(parts)
|
| 749 |
|
|
|
|
| 1116 |
return "no assessment recorded"
|
| 1117 |
overall = record.get("overall") if isinstance(record.get("overall"), Mapping) else {}
|
| 1118 |
evidence = record.get("evidence") if isinstance(record.get("evidence"), Mapping) else {}
|
|
|
|
| 1119 |
letter = _letter(overall.get("letter"))
|
| 1120 |
if rules.get("impact") and str(overall.get("state") or "") == "grade" and letter:
|
| 1121 |
assessed = overall.get("areas_assessed")
|
|
|
|
| 1253 |
tools: tuple[str, ...] = ()
|
| 1254 |
try:
|
| 1255 |
import distinct_tools
|
| 1256 |
+
from distinct_tools.skills import REPOSITORY_SPECS
|
| 1257 |
+
|
| 1258 |
+
# The installable set plus the digest-pinned repository: the
|
| 1259 |
+
# repository members are exactly as offerable by a worker, and a
|
| 1260 |
+
# catalogue that omits them silently strips them from every
|
| 1261 |
+
# operator approval β which is how a worker that offered a
|
| 1262 |
+
# repository skill ended up approved for none of them.
|
| 1263 |
+
members = tuple(distinct_tools.INSTALLABLE_SPECS) + tuple(REPOSITORY_SPECS)
|
| 1264 |
+
tools = tuple(sorted(f"{spec.tool_id}@{spec.version}" for spec in members))
|
| 1265 |
except Exception:
|
| 1266 |
tools = ()
|
| 1267 |
return Allowance(models=tuple(MODEL_CATALOG), tools=tools)
|
|
|
|
| 1346 |
|
| 1347 |
# Library ----------------------------------------------------------
|
| 1348 |
|
| 1349 |
+
def _advertised_library_refs(self, viewer_id: Optional[str] = None) -> tuple[str, ...]:
|
| 1350 |
+
"""Every exact ``id@version`` an agent this viewer may see advertises.
|
| 1351 |
+
|
| 1352 |
+
The viewer matters: ``list_agents`` filters by visibility, so asking
|
| 1353 |
+
with no viewer returns nothing at all. That is exactly what happened β
|
| 1354 |
+
every caller here asked viewer-less, the picker's choice list was
|
| 1355 |
+
permanently empty, and "no tools installed" showed beside workers
|
| 1356 |
+
that were advertising a dozen. The library page's descriptions are
|
| 1357 |
+
public; which of them is *offered right now* is per-viewer.
|
| 1358 |
+
"""
|
| 1359 |
|
| 1360 |
refs: set[str] = set()
|
| 1361 |
try:
|
| 1362 |
+
for view in self.control.list_agents(viewer_id=viewer_id):
|
| 1363 |
for item in view.capabilities.tools:
|
| 1364 |
refs.add(item if "@" in item else f"{item}@1.0.0")
|
| 1365 |
except ControlPlaneError:
|
| 1366 |
pass
|
| 1367 |
return tuple(sorted(refs))
|
| 1368 |
|
| 1369 |
+
def _library_choices(self, viewer_id: Optional[str] = None) -> list[tuple[str, str]]:
|
| 1370 |
"""Options for the composer's library picker, named for people.
|
| 1371 |
|
| 1372 |
The label leads with a readable name and its kind, because that is
|
|
|
|
| 1377 |
|
| 1378 |
specs = _library_specs()
|
| 1379 |
choices: list[tuple[str, str]] = []
|
| 1380 |
+
for ref in self._advertised_library_refs(viewer_id):
|
| 1381 |
spec = specs.get(ref)
|
| 1382 |
if spec is None:
|
| 1383 |
choices.append((f"{ref} Β· library member", ref))
|
|
|
|
| 1404 |
)
|
| 1405 |
return render.library_help(rows)
|
| 1406 |
|
| 1407 |
+
def _tool_selections(
|
| 1408 |
+
self,
|
| 1409 |
+
values: Optional[Sequence[str]],
|
| 1410 |
+
viewer_id: Optional[str] = None,
|
| 1411 |
+
) -> tuple[ToolSelection, ...]:
|
| 1412 |
+
advertised = frozenset(self._advertised_library_refs(viewer_id))
|
| 1413 |
result = []
|
| 1414 |
for value in values or ():
|
| 1415 |
if not isinstance(value, str) or "@" not in value:
|
|
|
|
| 1428 |
selected: Optional[Sequence[str]] = None,
|
| 1429 |
*,
|
| 1430 |
defaults: Optional[Sequence[str]] = None,
|
| 1431 |
+
viewer_id: Optional[str] = None,
|
| 1432 |
) -> tuple[Any, str]:
|
| 1433 |
"""The refreshed selector plus its undernote, preserving selection.
|
| 1434 |
|
|
|
|
| 1441 |
what a run starts with, never what it is stuck with.
|
| 1442 |
"""
|
| 1443 |
|
| 1444 |
+
choices = self._library_choices(viewer_id)
|
| 1445 |
valid = {ref for _, ref in choices}
|
| 1446 |
if selected is None and defaults:
|
| 1447 |
value = [ref for ref in defaults if ref in valid]
|
|
|
|
| 1485 |
rows.append({"ref": ref, **spec})
|
| 1486 |
return rows[: self.LIBRARY_PAGE_LIMIT], len(rows)
|
| 1487 |
|
| 1488 |
+
def _library_page(
|
| 1489 |
+
self, query: str, defaults: Sequence[str]
|
| 1490 |
+
) -> tuple[Any, str, str]:
|
| 1491 |
+
"""The page a person picks their defaults on: cards, summary, picker."""
|
| 1492 |
|
| 1493 |
rows, matched = self._library_rows(query)
|
| 1494 |
total = len(_library_specs())
|
|
|
|
| 1507 |
render.library_summary(
|
| 1508 |
shown=len(rows), matched=matched, total=total, chosen=len(kept)
|
| 1509 |
),
|
| 1510 |
+
render.library_cards(rows, kept),
|
| 1511 |
)
|
| 1512 |
|
| 1513 |
def update_library_page(
|
| 1514 |
self, query: str, state: Mapping[str, Any]
|
| 1515 |
+
) -> tuple[Any, str, str]:
|
| 1516 |
"""Re-filter the page without touching what has been chosen."""
|
| 1517 |
|
| 1518 |
return self._library_page(query, _library_defaults(state))
|
|
|
|
| 1522 |
visible_choice: Sequence[str],
|
| 1523 |
query: str,
|
| 1524 |
state: Mapping[str, Any],
|
| 1525 |
+
profile: "gr.OAuthProfile | None" = None,
|
| 1526 |
+
request: "gr.Request | None" = None,
|
| 1527 |
+
) -> tuple[Any, Any, str, str, Any, str]:
|
| 1528 |
"""Record the defaults, merging the visible page into the kept set.
|
| 1529 |
|
| 1530 |
The subtlety is that the component only reports what it can see. A
|
|
|
|
| 1543 |
}
|
| 1544 |
specs = _library_specs()
|
| 1545 |
updated = {**state, "library_defaults": sorted(ref for ref in chosen if ref in specs)}
|
| 1546 |
+
try:
|
| 1547 |
+
viewer_id = self._authorize(dict(state), profile, request)
|
| 1548 |
+
except LoginRequired:
|
| 1549 |
+
viewer_id = None
|
| 1550 |
selector, note = self._library_components(
|
| 1551 |
+
_library_defaults(updated),
|
| 1552 |
+
defaults=_library_defaults(updated),
|
| 1553 |
+
viewer_id=viewer_id,
|
| 1554 |
)
|
| 1555 |
+
_, summary, cards = self._library_page(query, _library_defaults(updated))
|
| 1556 |
+
return updated, updated, summary, cards, selector, note
|
| 1557 |
|
| 1558 |
# Outputs ----------------------------------------------------------
|
| 1559 |
|
|
|
|
| 1864 |
DEFAULT_MODEL_ID, (), "auto", MODE_SIMPLE, viewer_id=user_id
|
| 1865 |
)
|
| 1866 |
library, library_note = self._library_components(
|
| 1867 |
+
None, defaults=_library_defaults(updated), viewer_id=user_id
|
| 1868 |
)
|
| 1869 |
+
page, page_summary, page_cards = self._library_page("", _library_defaults(updated))
|
| 1870 |
return (
|
| 1871 |
updated,
|
| 1872 |
updated,
|
|
|
|
| 1887 |
# the moment the accordion is expanded.
|
| 1888 |
page,
|
| 1889 |
page_summary,
|
| 1890 |
+
page_cards,
|
| 1891 |
)
|
| 1892 |
|
| 1893 |
def _live_steps(self, view: Any) -> list[Mapping[str, Any]]:
|
|
|
|
| 1981 |
"""
|
| 1982 |
|
| 1983 |
try:
|
| 1984 |
+
tools = self._tool_selections(tool_values, viewer_id)
|
| 1985 |
views = self.control.compatible_agents(
|
| 1986 |
model_id, tools, viewer_id=viewer_id, include_offline=True
|
| 1987 |
)
|
|
|
|
| 2269 |
raise ValueError("Enter a request")
|
| 2270 |
if model_id not in MODEL_CATALOG:
|
| 2271 |
raise ValueError("Select a supported model")
|
| 2272 |
+
tools = self._tool_selections(tool_values, user_id)
|
| 2273 |
conversation = active_conversation(state)
|
| 2274 |
parent_job_id = conversation["job_ids"][-1] if conversation["job_ids"] else None
|
| 2275 |
stored = self._store_attachments(state["session_id"], uploads or ())
|
|
|
|
| 2362 |
# CheckboxGroup on every tick races the user's own clicks: a box
|
| 2363 |
# ticked while a tick was in flight would be silently unticked by the
|
| 2364 |
# stale value coming back.
|
| 2365 |
+
current_refs = self._advertised_library_refs(viewer_id)
|
| 2366 |
+
cache = getattr(self, "_last_library_refs", None)
|
| 2367 |
+
if not isinstance(cache, dict):
|
| 2368 |
+
cache = {}
|
| 2369 |
+
self._last_library_refs = cache
|
| 2370 |
+
if current_refs != cache.get(viewer_id):
|
| 2371 |
+
cache[viewer_id] = current_refs
|
| 2372 |
library, library_note = self._library_components(
|
| 2373 |
+
tool_values or None,
|
| 2374 |
+
defaults=_library_defaults(updated),
|
| 2375 |
+
viewer_id=viewer_id,
|
| 2376 |
)
|
| 2377 |
else:
|
| 2378 |
library, library_note = gr.skip(), gr.skip()
|
|
|
|
| 2972 |
pass
|
| 2973 |
return ""
|
| 2974 |
|
| 2975 |
+
def _join_panel(self, code: str = "", state: Mapping[str, Any] | None = None) -> str:
|
| 2976 |
"""The address an agent should be pointed at, and the code if minted.
|
| 2977 |
|
| 2978 |
The port comes from the same policy the launch uses, so the panel
|
|
|
|
| 3003 |
qr = qr_svg(addresses[0])
|
| 3004 |
except Exception: # noqa: BLE001 - the address alone is enough
|
| 3005 |
qr = ""
|
| 3006 |
+
# The command mirrors the defaults this browser chose on the library
|
| 3007 |
+
# page, so what the person copies is what their runs will expect.
|
| 3008 |
+
chosen = _library_defaults(state) if state is not None else ()
|
| 3009 |
+
tools = ",".join(chosen) if chosen else "local"
|
| 3010 |
+
return render.join_this_server(addresses=addresses, code=code, qr=qr, tools=tools)
|
| 3011 |
|
| 3012 |
# Pairing/downloads ------------------------------------------------
|
| 3013 |
|
|
|
|
| 3020 |
try:
|
| 3021 |
user_id = self._authorize(state, profile, request)
|
| 3022 |
value = self.control.create_pairing_code(user_id)
|
| 3023 |
+
return self._join_panel(value.code, state)
|
| 3024 |
except (ControlPlaneError, LoginRequired, ValueError) as exc:
|
| 3025 |
raise gr.Error(str(exc)) from exc
|
| 3026 |
|
|
|
|
| 3109 |
# button below is the one sign-in control, so a header link
|
| 3110 |
# would only duplicate it.
|
| 3111 |
identity = gr.HTML(_identity_html(""))
|
| 3112 |
+
# The quick nav sits directly under the identity box: one
|
| 3113 |
+
# anchor per page-worthy section. Links, not buttons, because
|
| 3114 |
+
# they navigate rather than act.
|
| 3115 |
+
gr.HTML(
|
| 3116 |
+
'<nav class="c-quicknav" aria-label="Sections">'
|
| 3117 |
+
'<a class="c-quicknav__link" href="#library">Library</a>'
|
| 3118 |
+
'<a class="c-quicknav__link" href="#join-agent">Run an agent</a>'
|
| 3119 |
+
"</nav>"
|
| 3120 |
+
)
|
| 3121 |
|
| 3122 |
# THE SIGN-IN CONTROL, AND WHY IT IS CONDITIONAL.
|
| 3123 |
#
|
|
|
|
| 3284 |
)
|
| 3285 |
|
| 3286 |
with gr.Column(elem_classes="c-conversation", min_width=0):
|
| 3287 |
+
# The no-agents veil. Static markup; presentation.py
|
| 3288 |
+
# shows it with :has() only while somebody is signed in
|
| 3289 |
+
# and the worker panel is in its empty state, so there
|
| 3290 |
+
# is no event plumbing to fall out of date. It softly
|
| 3291 |
+
# fades the chat rather than hiding it: the interface
|
| 3292 |
+
# is not broken, it is waiting for a worker.
|
| 3293 |
+
gr.HTML(
|
| 3294 |
+
'<div class="c-veil" role="status">'
|
| 3295 |
+
'<div class="c-veil__panel">'
|
| 3296 |
+
'<p class="c-veil__title">No agents available</p>'
|
| 3297 |
+
'<p class="c-veil__body">Nothing can run until a '
|
| 3298 |
+
"community agent pairs with this server. Set one up "
|
| 3299 |
+
"in a minute, or come back when one is online.</p>"
|
| 3300 |
+
'<a class="c-veil__action" href="#join-agent">'
|
| 3301 |
+
"Set up an agent</a>"
|
| 3302 |
+
"</div></div>"
|
| 3303 |
+
)
|
| 3304 |
chat = gr.Chatbot(
|
| 3305 |
label="Conversation",
|
| 3306 |
show_label=False,
|
|
|
|
| 3421 |
# left edge of their own. Inside the tail they share the shell's
|
| 3422 |
# padding box like everything else.
|
| 3423 |
with gr.Column(elem_classes="c-tail"):
|
| 3424 |
+
with gr.Accordion(
|
| 3425 |
+
"Run a community agent", open=False, elem_id="join-agent"
|
| 3426 |
+
):
|
| 3427 |
gr.Markdown(
|
| 3428 |
# "The agent owns its bounded FIFO and reports it
|
| 3429 |
# on every poll" is true and is a sentence for
|
|
|
|
| 3453 |
"Download agent", variant="secondary"
|
| 3454 |
)
|
| 3455 |
create_code = gr.Button("Generate pairing code")
|
| 3456 |
+
gr.Markdown()
|
| 3457 |
|
| 3458 |
# THE LIBRARY, AT THE SIZE IT IS GOING TO BE.
|
| 3459 |
#
|
|
|
|
| 3464 |
# person does here is choose their defaults once; those
|
| 3465 |
# are remembered in this browser and tick themselves on
|
| 3466 |
# every run where a worker offers them.
|
| 3467 |
+
with gr.Accordion(
|
| 3468 |
+
"Library", open=True, elem_id="library",
|
| 3469 |
+
elem_classes="c-library-section",
|
| 3470 |
+
):
|
| 3471 |
gr.Markdown(
|
| 3472 |
+
"Every tool and skill this server can offer, who wrote "
|
| 3473 |
+
"it, and what it does. Hover a card β or press and hold "
|
| 3474 |
+
"on a phone β to watch its demonstration. Tick the "
|
| 3475 |
+
"members you usually want below: they select themselves "
|
| 3476 |
+
"whenever a worker offers them, and any run can untick "
|
| 3477 |
+
"them without changing what is saved."
|
| 3478 |
)
|
| 3479 |
library_search = gr.Textbox(
|
| 3480 |
label="Search the library",
|
|
|
|
| 3486 |
library_summary = gr.HTML(
|
| 3487 |
render.library_summary(shown=0, matched=0, total=0, chosen=0)
|
| 3488 |
)
|
| 3489 |
+
library_cards_html = gr.HTML(elem_classes="c-librarycards")
|
| 3490 |
+
gr.Markdown("**Your defaults**", elem_classes="c-librarydefaults-head")
|
| 3491 |
library_defaults = gr.CheckboxGroup(
|
| 3492 |
label="Your defaults",
|
| 3493 |
show_label=False,
|
|
|
|
| 3545 |
activity_panel,
|
| 3546 |
library_defaults,
|
| 3547 |
library_summary,
|
| 3548 |
+
library_cards_html,
|
| 3549 |
],
|
| 3550 |
queue=False,
|
| 3551 |
api_visibility="private",
|
|
|
|
| 3746 |
library_search.input(
|
| 3747 |
self.update_library_page,
|
| 3748 |
inputs=[library_search, session_state],
|
| 3749 |
+
outputs=[library_defaults, library_summary, library_cards_html],
|
| 3750 |
queue=False,
|
| 3751 |
api_visibility="private",
|
| 3752 |
)
|
|
|
|
| 3757 |
session_state,
|
| 3758 |
browser_state,
|
| 3759 |
library_summary,
|
| 3760 |
+
library_cards_html,
|
| 3761 |
tools,
|
| 3762 |
library_note,
|
| 3763 |
],
|
distinct_skills/handlers.py
CHANGED
|
@@ -32,7 +32,7 @@ import base64
|
|
| 32 |
import csv
|
| 33 |
import io
|
| 34 |
import re
|
| 35 |
-
from collections.abc import Mapping
|
| 36 |
|
| 37 |
from .loader import (
|
| 38 |
MAX_DESCRIPTION_CHARACTERS,
|
|
@@ -363,6 +363,637 @@ def csv_table(arguments: Mapping[str, object], skill: SkillDefinition) -> dict[s
|
|
| 363 |
}
|
| 364 |
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
#: The fixed handler table. A skill directory may name one of these and
|
| 367 |
#: nothing else.
|
| 368 |
HANDLERS: Mapping[str, object] = {
|
|
@@ -370,6 +1001,12 @@ HANDLERS: Mapping[str, object] = {
|
|
| 370 |
"skill_scaffold": skill_scaffold,
|
| 371 |
"review_checklist": review_checklist,
|
| 372 |
"csv_table": csv_table,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
}
|
| 374 |
|
| 375 |
HANDLER_NAMES = frozenset(HANDLERS)
|
|
|
|
| 32 |
import csv
|
| 33 |
import io
|
| 34 |
import re
|
| 35 |
+
from collections.abc import Mapping, Sequence
|
| 36 |
|
| 37 |
from .loader import (
|
| 38 |
MAX_DESCRIPTION_CHARACTERS,
|
|
|
|
| 363 |
}
|
| 364 |
|
| 365 |
|
| 366 |
+
# --------------------------------------------------------------------------
|
| 367 |
+
# Binary artifacts, and the office formats built as zips of XML
|
| 368 |
+
# --------------------------------------------------------------------------
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def _multiline(arguments: Mapping[str, object], name: str, *, limit: int) -> str:
|
| 372 |
+
"""Like ``_line`` but newlines are structure here, so they survive.
|
| 373 |
+
|
| 374 |
+
Each line is cleaned individually; the line breaks between them are the
|
| 375 |
+
input format for every handler below that reads rows, slides or sections.
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
value = arguments.get(name)
|
| 379 |
+
if not isinstance(value, str) or not value.strip():
|
| 380 |
+
raise SkillInputError(f"{name} must be a non-empty string")
|
| 381 |
+
if len(value) > limit:
|
| 382 |
+
raise SkillInputError(f"{name} must be at most {limit} characters")
|
| 383 |
+
lines = [" ".join(clean_text(line, limit).split()) for line in value.splitlines()]
|
| 384 |
+
return "\n".join(lines)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def bytes_artifact(name: str, media_type: str, payload: bytes) -> dict[str, object]:
|
| 388 |
+
"""The artifact envelope for content that is bytes rather than text.
|
| 389 |
+
|
| 390 |
+
``text_artifact`` above encodes prose. The office formats are zip
|
| 391 |
+
archives, so they arrive here already encoded; wrapping them through a
|
| 392 |
+
text path would corrupt them on the first non-ASCII byte.
|
| 393 |
+
"""
|
| 394 |
+
|
| 395 |
+
return {
|
| 396 |
+
"name": name,
|
| 397 |
+
"media_type": media_type,
|
| 398 |
+
"base64": base64.b64encode(payload).decode("ascii"),
|
| 399 |
+
"size_bytes": len(payload),
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def _xml_escape(value: str) -> str:
|
| 404 |
+
return (
|
| 405 |
+
value.replace("&", "&")
|
| 406 |
+
.replace("<", "<")
|
| 407 |
+
.replace(">", ">")
|
| 408 |
+
.replace('"', """)
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def _zip_bytes(parts: Mapping[str, str]) -> bytes:
|
| 413 |
+
"""One deterministic zip: fixed timestamps, fixed order, deflate.
|
| 414 |
+
|
| 415 |
+
Deterministic because two runs with the same arguments should produce
|
| 416 |
+
byte-identical files; a timestamp that encodes the wall clock would make
|
| 417 |
+
every artifact unique for no reason a user can see.
|
| 418 |
+
"""
|
| 419 |
+
|
| 420 |
+
import io
|
| 421 |
+
import zipfile
|
| 422 |
+
|
| 423 |
+
buffer = io.BytesIO()
|
| 424 |
+
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
| 425 |
+
for name in parts:
|
| 426 |
+
info = zipfile.ZipInfo(name, date_time=(2020, 1, 1, 0, 0, 0))
|
| 427 |
+
info.compress_type = zipfile.ZIP_DEFLATED
|
| 428 |
+
archive.writestr(info, parts[name])
|
| 429 |
+
return buffer.getvalue()
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
_SEPARATORS: Mapping[str, str] = {"pipe": "|", "tab": "\t", "semicolon": ";"}
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def _rows_from_text(arguments: Mapping[str, object]) -> list[list[str]]:
|
| 436 |
+
"""Header-then-rows lines, split on the named separator, shape-checked.
|
| 437 |
+
|
| 438 |
+
The same input convention as ``csv_table`` and for the same reason: a
|
| 439 |
+
comma would collide with commas inside the data, and a model below 7B
|
| 440 |
+
handles "one line per row, fields split by a pipe" reliably.
|
| 441 |
+
"""
|
| 442 |
+
|
| 443 |
+
text = _multiline(arguments, "text", limit=6000)
|
| 444 |
+
separator_name = str(arguments.get("separator") or "pipe")
|
| 445 |
+
if separator_name not in _SEPARATORS:
|
| 446 |
+
raise SkillInputError("separator must be pipe, tab or semicolon")
|
| 447 |
+
separator = _SEPARATORS[separator_name]
|
| 448 |
+
lines = [line for line in text.splitlines() if line.strip()]
|
| 449 |
+
if len(lines) < 2:
|
| 450 |
+
raise SkillInputError("text needs a header line and at least one row")
|
| 451 |
+
rows = [[cell.strip() for cell in line.split(separator)] for line in lines]
|
| 452 |
+
width = len(rows[0])
|
| 453 |
+
for index, row in enumerate(rows[1:], start=2):
|
| 454 |
+
if len(row) != width:
|
| 455 |
+
raise SkillInputError(
|
| 456 |
+
f"line {index} has {len(row)} fields where the header has {width}. "
|
| 457 |
+
"Fix the row rather than letting a column silently shift"
|
| 458 |
+
)
|
| 459 |
+
return rows
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
_DOCX_CONTENT_TYPES = (
|
| 463 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 464 |
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
| 465 |
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
| 466 |
+
'<Default Extension="xml" ContentType="application/xml"/>'
|
| 467 |
+
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>'
|
| 468 |
+
'<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>'
|
| 469 |
+
"</Types>"
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
_DOCX_ROOT_RELS = (
|
| 473 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 474 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 475 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>'
|
| 476 |
+
"</Relationships>"
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
_DOCX_DOCUMENT_RELS = (
|
| 480 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 481 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 482 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>'
|
| 483 |
+
"</Relationships>"
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
_DOCX_STYLES = (
|
| 487 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 488 |
+
'<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">'
|
| 489 |
+
'<w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/>'
|
| 490 |
+
'<w:rPr><w:b/><w:sz w:val="48"/></w:rPr></w:style>'
|
| 491 |
+
'<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/>'
|
| 492 |
+
'<w:rPr><w:b/><w:sz w:val="32"/></w:rPr></w:style>'
|
| 493 |
+
"</w:styles>"
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
def _docx_paragraph(text: str, style: str | None = None) -> str:
|
| 500 |
+
properties = f'<w:pPr><w:pStyle w:val="{style}"/></w:pPr>' if style else ""
|
| 501 |
+
return (
|
| 502 |
+
f"<w:p>{properties}<w:r><w:t xml:space=\"preserve\">"
|
| 503 |
+
f"{_xml_escape(text)}</w:t></w:r></w:p>"
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def document_docx(arguments: Mapping[str, object], skill: SkillDefinition) -> dict[str, object]:
|
| 508 |
+
"""A real, minimal ``.docx``: title, headings and paragraphs.
|
| 509 |
+
|
| 510 |
+
Lines are paragraphs. A line beginning ``# `` becomes a heading. That is
|
| 511 |
+
the entire input language, because the point of this skill is that the
|
| 512 |
+
model supplies sentences and the format arithmetic happens here.
|
| 513 |
+
"""
|
| 514 |
+
|
| 515 |
+
del skill
|
| 516 |
+
_known(arguments, frozenset({"title", "body", "filename"}))
|
| 517 |
+
title = _line(arguments, "title", limit=200)
|
| 518 |
+
body = _multiline(arguments, "body", limit=6000)
|
| 519 |
+
blocks = [_docx_paragraph(title, "Title")]
|
| 520 |
+
for raw_line in body.splitlines():
|
| 521 |
+
line = raw_line.strip()
|
| 522 |
+
if not line:
|
| 523 |
+
continue
|
| 524 |
+
if line.startswith("# "):
|
| 525 |
+
blocks.append(_docx_paragraph(line[2:].strip(), "Heading1"))
|
| 526 |
+
else:
|
| 527 |
+
blocks.append(_docx_paragraph(line))
|
| 528 |
+
document = (
|
| 529 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 530 |
+
f'<w:document xmlns:w="{_W}"><w:body>' + "".join(blocks) + "</w:body></w:document>"
|
| 531 |
+
)
|
| 532 |
+
payload = _zip_bytes(
|
| 533 |
+
{
|
| 534 |
+
"[Content_Types].xml": _DOCX_CONTENT_TYPES,
|
| 535 |
+
"_rels/.rels": _DOCX_ROOT_RELS,
|
| 536 |
+
"word/_rels/document.xml.rels": _DOCX_DOCUMENT_RELS,
|
| 537 |
+
"word/styles.xml": _DOCX_STYLES,
|
| 538 |
+
"word/document.xml": document,
|
| 539 |
+
}
|
| 540 |
+
)
|
| 541 |
+
return {
|
| 542 |
+
"artifact": bytes_artifact(
|
| 543 |
+
_filename(arguments.get("filename"), "document.docx", ".docx"),
|
| 544 |
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
| 545 |
+
payload,
|
| 546 |
+
)
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
_XLSX_CONTENT_TYPES = (
|
| 551 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 552 |
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
| 553 |
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
| 554 |
+
'<Default Extension="xml" ContentType="application/xml"/>'
|
| 555 |
+
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
|
| 556 |
+
'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
|
| 557 |
+
"</Types>"
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
_XLSX_ROOT_RELS = (
|
| 561 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 562 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 563 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
|
| 564 |
+
"</Relationships>"
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
_XLSX_WORKBOOK = (
|
| 568 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 569 |
+
'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
|
| 570 |
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
|
| 571 |
+
'<sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets></workbook>'
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
_XLSX_WORKBOOK_RELS = (
|
| 575 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 576 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 577 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
|
| 578 |
+
"</Relationships>"
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
def _column_name(index: int) -> str:
|
| 583 |
+
name = ""
|
| 584 |
+
while index >= 0:
|
| 585 |
+
name = chr(ord("A") + index % 26) + name
|
| 586 |
+
index = index // 26 - 1
|
| 587 |
+
return name
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
def _xlsx_cell(reference: str, value: str) -> str:
|
| 591 |
+
if _looks_numeric(value):
|
| 592 |
+
return f'<c r="{reference}" t="n"><v>{value.strip()}</v></c>'
|
| 593 |
+
return (
|
| 594 |
+
f'<c r="{reference}" t="inlineStr"><is><t xml:space="preserve">'
|
| 595 |
+
f"{_xml_escape(value)}</t></is></c>"
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def spreadsheet_xlsx(arguments: Mapping[str, object], skill: SkillDefinition) -> dict[str, object]:
|
| 600 |
+
"""A real ``.xlsx`` from header-then-rows text.
|
| 601 |
+
|
| 602 |
+
Numbers become number cells so the spreadsheet can sum them; everything
|
| 603 |
+
else is an inline string, so nothing here can become a live formula. The
|
| 604 |
+
same guard reasoning as ``csv_table``: a cell of untrusted text must not
|
| 605 |
+
run when the file opens.
|
| 606 |
+
"""
|
| 607 |
+
|
| 608 |
+
del skill
|
| 609 |
+
_known(arguments, frozenset({"text", "separator", "filename"}))
|
| 610 |
+
rows = _rows_from_text(arguments)
|
| 611 |
+
body: list[str] = []
|
| 612 |
+
for row_index, row in enumerate(rows, start=1):
|
| 613 |
+
cells = []
|
| 614 |
+
for column_index, value in enumerate(row):
|
| 615 |
+
reference = f"{_column_name(column_index)}{row_index}"
|
| 616 |
+
guarded, _ = _guard(value)
|
| 617 |
+
cells.append(_xlsx_cell(reference, guarded))
|
| 618 |
+
body.append(f'<row r="{row_index}">' + "".join(cells) + "</row>")
|
| 619 |
+
sheet = (
|
| 620 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 621 |
+
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
|
| 622 |
+
"<sheetData>" + "".join(body) + "</sheetData></worksheet>"
|
| 623 |
+
)
|
| 624 |
+
payload = _zip_bytes(
|
| 625 |
+
{
|
| 626 |
+
"[Content_Types].xml": _XLSX_CONTENT_TYPES,
|
| 627 |
+
"_rels/.rels": _XLSX_ROOT_RELS,
|
| 628 |
+
"xl/workbook.xml": _XLSX_WORKBOOK,
|
| 629 |
+
"xl/_rels/workbook.xml.rels": _XLSX_WORKBOOK_RELS,
|
| 630 |
+
"xl/worksheets/sheet1.xml": sheet,
|
| 631 |
+
}
|
| 632 |
+
)
|
| 633 |
+
return {
|
| 634 |
+
"artifact": bytes_artifact(
|
| 635 |
+
_filename(arguments.get("filename"), "table.xlsx", ".xlsx"),
|
| 636 |
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 637 |
+
payload,
|
| 638 |
+
)
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
# --------------------------------------------------------------------------
|
| 643 |
+
# Slide decks: the smallest package python-pptx and PowerPoint both accept
|
| 644 |
+
# --------------------------------------------------------------------------
|
| 645 |
+
|
| 646 |
+
_PPTX_CONTENT_TYPES_HEAD = (
|
| 647 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 648 |
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
| 649 |
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
| 650 |
+
'<Default Extension="xml" ContentType="application/xml"/>'
|
| 651 |
+
'<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>'
|
| 652 |
+
'<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>'
|
| 653 |
+
'<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>'
|
| 654 |
+
'<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>'
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
_PPTX_ROOT_RELS = (
|
| 658 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 659 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 660 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>'
|
| 661 |
+
"</Relationships>"
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
_PPTX_THEME = (
|
| 665 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 666 |
+
'<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="distinct">'
|
| 667 |
+
'<a:themeElements>'
|
| 668 |
+
'<a:clrScheme name="distinct">'
|
| 669 |
+
'<a:dk1><a:srgbClr val="203127"/></a:dk1><a:lt1><a:srgbClr val="F7F9F3"/></a:lt1>'
|
| 670 |
+
'<a:dk2><a:srgbClr val="2E4636"/></a:dk2><a:lt2><a:srgbClr val="EDF1E6"/></a:lt2>'
|
| 671 |
+
'<a:accent1><a:srgbClr val="4C7A5E"/></a:accent1><a:accent2><a:srgbClr val="6E9A7F"/></a:accent2>'
|
| 672 |
+
'<a:accent3><a:srgbClr val="A5C1AE"/></a:accent3><a:accent4><a:srgbClr val="C9D9CC"/></a:accent4>'
|
| 673 |
+
'<a:accent5><a:srgbClr val="8AA893"/></a:accent5><a:accent6><a:srgbClr val="5C8A6D"/></a:accent6>'
|
| 674 |
+
'<a:hlink><a:srgbClr val="2E5E43"/></a:hlink><a:folHlink><a:srgbClr val="2E5E43"/></a:folHlink>'
|
| 675 |
+
"</a:clrScheme>"
|
| 676 |
+
'<a:fontScheme name="distinct"><a:majorFont><a:latin typeface="Georgia"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>'
|
| 677 |
+
'<a:minorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme>'
|
| 678 |
+
'<a:fmtScheme name="distinct">'
|
| 679 |
+
"<a:fillStyleLst><a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill>"
|
| 680 |
+
"<a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill><a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill></a:fillStyleLst>"
|
| 681 |
+
'<a:lnStyleLst><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>'
|
| 682 |
+
'<a:ln w="12700"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>'
|
| 683 |
+
'<a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst>'
|
| 684 |
+
"<a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle>"
|
| 685 |
+
"<a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>"
|
| 686 |
+
"<a:bgFillStyleLst><a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill>"
|
| 687 |
+
"<a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill><a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill></a:bgFillStyleLst>"
|
| 688 |
+
"</a:fmtScheme></a:themeElements></a:theme>"
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
+
_PPTX_MASTER = (
|
| 692 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 693 |
+
'<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
| 694 |
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
|
| 695 |
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
|
| 696 |
+
'<p:cSld><p:bg><p:bgPr><a:solidFill><a:schemeClr val="lt1"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>'
|
| 697 |
+
"<p:spTree>"
|
| 698 |
+
'<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>'
|
| 699 |
+
"<p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"0\" cy=\"0\"/>"
|
| 700 |
+
"<a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"0\" cy=\"0\"/></a:xfrm></p:grpSpPr>"
|
| 701 |
+
"</p:spTree></p:cSld>"
|
| 702 |
+
'<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" '
|
| 703 |
+
'accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>'
|
| 704 |
+
'<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>'
|
| 705 |
+
"</p:sldMaster>"
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
_PPTX_MASTER_RELS = (
|
| 709 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 710 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 711 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>'
|
| 712 |
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>'
|
| 713 |
+
"</Relationships>"
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
_PPTX_LAYOUT = (
|
| 717 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 718 |
+
'<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
| 719 |
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
|
| 720 |
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank">'
|
| 721 |
+
"<p:cSld><p:spTree>"
|
| 722 |
+
'<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>'
|
| 723 |
+
"<p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"0\" cy=\"0\"/>"
|
| 724 |
+
"<a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"0\" cy=\"0\"/></a:xfrm></p:grpSpPr>"
|
| 725 |
+
"</p:spTree></p:cSld>"
|
| 726 |
+
"</p:sldLayout>"
|
| 727 |
+
)
|
| 728 |
+
|
| 729 |
+
_PPTX_LAYOUT_RELS = (
|
| 730 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 731 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 732 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>'
|
| 733 |
+
"</Relationships>"
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
_PPTX_SLIDE_RELS = (
|
| 737 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 738 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 739 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>'
|
| 740 |
+
"</Relationships>"
|
| 741 |
+
)
|
| 742 |
+
|
| 743 |
+
#: EMU geometry for a 16:9 slide: full width 12192000, height 6858000.
|
| 744 |
+
_PPTX_TITLE_SHAPE = (
|
| 745 |
+
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="Title"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>'
|
| 746 |
+
"<p:nvPr/></p:nvSpPr>"
|
| 747 |
+
'<p:spPr><a:xfrm><a:off x="838200" y="838200"/><a:ext cx="10515600" cy="1325563"/></a:xfrm>'
|
| 748 |
+
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>'
|
| 749 |
+
"<p:txBody><a:bodyPr/><a:lstStyle/>"
|
| 750 |
+
'<a:p><a:pPr/><a:r><a:rPr lang="en" sz="3600" b="1"><a:solidFill><a:schemeClr val="dk1"/></a:solidFill></a:rPr>'
|
| 751 |
+
"<a:t>{title}</a:t></a:r></a:p></p:txBody></p:sp>"
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
_PPTX_BODY_SHAPE_HEAD = (
|
| 755 |
+
'<p:sp><p:nvSpPr><p:cNvPr id="3" name="Body"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>'
|
| 756 |
+
"<p:nvPr/></p:nvSpPr>"
|
| 757 |
+
'<p:spPr><a:xfrm><a:off x="838200" y="2438400"/><a:ext cx="10515600" cy="3581400"/></a:xfrm>'
|
| 758 |
+
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>'
|
| 759 |
+
"<p:txBody><a:bodyPr/><a:lstStyle/>"
|
| 760 |
+
)
|
| 761 |
+
|
| 762 |
+
|
| 763 |
+
def _pptx_slide(title: str, points: Sequence[str]) -> str:
|
| 764 |
+
paragraphs = []
|
| 765 |
+
for point in points:
|
| 766 |
+
paragraphs.append(
|
| 767 |
+
'<a:p><a:pPr marL="285750" indent="-285750"><a:buChar char="•"/></a:pPr>'
|
| 768 |
+
f'<a:r><a:rPr lang="en" sz="2000"><a:solidFill><a:schemeClr val="dk2"/></a:solidFill></a:rPr>'
|
| 769 |
+
f"<a:t>{_xml_escape(point)}</a:t></a:r></a:p>"
|
| 770 |
+
)
|
| 771 |
+
if not paragraphs:
|
| 772 |
+
paragraphs.append("<a:p><a:endParaRPr lang=\"en\"/></a:p>")
|
| 773 |
+
body = _PPTX_BODY_SHAPE_HEAD + "".join(paragraphs) + "</p:txBody></p:sp>"
|
| 774 |
+
return (
|
| 775 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 776 |
+
'<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
| 777 |
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
|
| 778 |
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
|
| 779 |
+
"<p:cSld><p:spTree>"
|
| 780 |
+
'<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>'
|
| 781 |
+
"<p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"0\" cy=\"0\"/>"
|
| 782 |
+
"<a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"0\" cy=\"0\"/></a:xfrm></p:grpSpPr>"
|
| 783 |
+
+ _PPTX_TITLE_SHAPE.replace("{title}", _xml_escape(title))
|
| 784 |
+
+ body
|
| 785 |
+
+ "</p:spTree></p:cSld></p:sld>"
|
| 786 |
+
)
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
def deck_pptx(arguments: Mapping[str, object], skill: SkillDefinition) -> dict[str, object]:
|
| 790 |
+
"""A real ``.pptx``: one slide per input line.
|
| 791 |
+
|
| 792 |
+
A line is ``Title :: point; point; point``. The double colon separates
|
| 793 |
+
the title from its points; the points split on semicolons. One line, one
|
| 794 |
+
slide, so the model never has to reason about the package format at all.
|
| 795 |
+
"""
|
| 796 |
+
|
| 797 |
+
del skill
|
| 798 |
+
_known(arguments, frozenset({"slides", "filename"}))
|
| 799 |
+
text = _multiline(arguments, "slides", limit=6000)
|
| 800 |
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
| 801 |
+
if not lines:
|
| 802 |
+
raise SkillInputError("slides needs at least one line")
|
| 803 |
+
if len(lines) > 30:
|
| 804 |
+
raise SkillInputError("a deck here is at most 30 slides")
|
| 805 |
+
|
| 806 |
+
slides: list[str] = []
|
| 807 |
+
for line in lines:
|
| 808 |
+
title, _, tail = line.partition("::")
|
| 809 |
+
points = [point.strip() for point in tail.split(";") if point.strip()]
|
| 810 |
+
slides.append(_pptx_slide(title.strip() or "Untitled", points[:10]))
|
| 811 |
+
|
| 812 |
+
parts: dict[str, str] = {}
|
| 813 |
+
overrides = []
|
| 814 |
+
slide_ids = []
|
| 815 |
+
slide_rels = []
|
| 816 |
+
for index, slide in enumerate(slides, start=1):
|
| 817 |
+
parts[f"ppt/slides/slide{index}.xml"] = slide
|
| 818 |
+
parts[f"ppt/slides/_rels/slide{index}.xml.rels"] = _PPTX_SLIDE_RELS
|
| 819 |
+
overrides.append(
|
| 820 |
+
f'<Override PartName="/ppt/slides/slide{index}.xml" '
|
| 821 |
+
'ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>'
|
| 822 |
+
)
|
| 823 |
+
slide_ids.append(f'<p:sldId id="{255 + index}" r:id="rId{2 + index}"/>')
|
| 824 |
+
slide_rels.append(
|
| 825 |
+
f'<Relationship Id="rId{2 + index}" '
|
| 826 |
+
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" '
|
| 827 |
+
f'Target="slides/slide{index}.xml"/>'
|
| 828 |
+
)
|
| 829 |
+
|
| 830 |
+
parts["[Content_Types].xml"] = _PPTX_CONTENT_TYPES_HEAD + "".join(overrides) + "</Types>"
|
| 831 |
+
parts["_rels/.rels"] = _PPTX_ROOT_RELS
|
| 832 |
+
parts["ppt/theme/theme1.xml"] = _PPTX_THEME
|
| 833 |
+
parts["ppt/slideMasters/slideMaster1.xml"] = _PPTX_MASTER
|
| 834 |
+
parts["ppt/slideMasters/_rels/slideMaster1.xml.rels"] = _PPTX_MASTER_RELS
|
| 835 |
+
parts["ppt/slideLayouts/slideLayout1.xml"] = _PPTX_LAYOUT
|
| 836 |
+
parts["ppt/slideLayouts/_rels/slideLayout1.xml.rels"] = _PPTX_LAYOUT_RELS
|
| 837 |
+
parts["ppt/presentation.xml"] = (
|
| 838 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 839 |
+
'<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
| 840 |
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
|
| 841 |
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
|
| 842 |
+
'<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>'
|
| 843 |
+
"<p:sldIdLst>" + "".join(slide_ids) + "</p:sldIdLst>"
|
| 844 |
+
'<p:sldSz cx="12192000" cy="6858000"/><p:notesSz cx="6858000" cy="9144000"/>'
|
| 845 |
+
"</p:presentation>"
|
| 846 |
+
)
|
| 847 |
+
parts["ppt/_rels/presentation.xml.rels"] = (
|
| 848 |
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
| 849 |
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
| 850 |
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>'
|
| 851 |
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>'
|
| 852 |
+
+ "".join(slide_rels)
|
| 853 |
+
+ "</Relationships>"
|
| 854 |
+
)
|
| 855 |
+
|
| 856 |
+
payload = _zip_bytes(parts)
|
| 857 |
+
return {
|
| 858 |
+
"artifact": bytes_artifact(
|
| 859 |
+
_filename(arguments.get("filename"), "deck.pptx", ".pptx"),
|
| 860 |
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
| 861 |
+
payload,
|
| 862 |
+
)
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
|
| 866 |
+
# --------------------------------------------------------------------------
|
| 867 |
+
# Web pages, themes and tailored guides
|
| 868 |
+
# --------------------------------------------------------------------------
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
def web_page(arguments: Mapping[str, object], skill: SkillDefinition) -> dict[str, object]:
|
| 872 |
+
"""One clean, self-contained HTML page from a title and sections.
|
| 873 |
+
|
| 874 |
+
A section is ``Heading :: body text`` per line. The page carries its own
|
| 875 |
+
small stylesheet, no scripts and no external requests, so what the user
|
| 876 |
+
downloads is complete and inert.
|
| 877 |
+
"""
|
| 878 |
+
|
| 879 |
+
del skill
|
| 880 |
+
_known(arguments, frozenset({"title", "sections", "accent", "filename"}))
|
| 881 |
+
title = _line(arguments, "title", limit=120)
|
| 882 |
+
text = _multiline(arguments, "sections", limit=6000)
|
| 883 |
+
accent = str(arguments.get("accent") or "#2E5E43").strip()
|
| 884 |
+
if not re.fullmatch(r"#[0-9A-Fa-f]{6}", accent):
|
| 885 |
+
raise SkillInputError("accent must be a #rrggbb colour")
|
| 886 |
+
blocks = []
|
| 887 |
+
for line in text.splitlines():
|
| 888 |
+
line = line.strip()
|
| 889 |
+
if not line:
|
| 890 |
+
continue
|
| 891 |
+
heading, _, body = line.partition("::")
|
| 892 |
+
blocks.append(
|
| 893 |
+
f"<section><h2>{_xml_escape(heading.strip())}</h2>"
|
| 894 |
+
f"<p>{_xml_escape(body.strip())}</p></section>"
|
| 895 |
+
)
|
| 896 |
+
if not blocks:
|
| 897 |
+
raise SkillInputError("sections needs at least one 'Heading :: body' line")
|
| 898 |
+
page = (
|
| 899 |
+
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">"
|
| 900 |
+
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
| 901 |
+
f"<title>{_xml_escape(title)}</title><style>"
|
| 902 |
+
":root{--accent:" + accent + ";}"
|
| 903 |
+
"body{margin:0;font:17px/1.6 Georgia,serif;color:#26302a;background:#faf9f5;}"
|
| 904 |
+
"main{max-width:680px;margin:0 auto;padding:64px 24px;}"
|
| 905 |
+
"h1{font-size:2.4rem;line-height:1.15;margin:0 0 8px;}"
|
| 906 |
+
"h1::after{content:'';display:block;width:56px;height:4px;background:var(--accent);margin-top:16px;}"
|
| 907 |
+
"h2{font-size:1.15rem;margin:40px 0 6px;color:var(--accent);"
|
| 908 |
+
"font-family:Arial,sans-serif;letter-spacing:.02em;}"
|
| 909 |
+
"p{margin:0;}"
|
| 910 |
+
"</style></head><body><main>"
|
| 911 |
+
f"<h1>{_xml_escape(title)}</h1>" + "".join(blocks) + "</main></body></html>"
|
| 912 |
+
)
|
| 913 |
+
return {
|
| 914 |
+
"artifact": text_artifact(
|
| 915 |
+
_filename(arguments.get("filename"), "page.html", ".html"), "text/html", page
|
| 916 |
+
)
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
|
| 920 |
+
def theme_css(arguments: Mapping[str, object], skill: SkillDefinition) -> dict[str, object]:
|
| 921 |
+
"""A CSS custom-property sheet from a named palette.
|
| 922 |
+
|
| 923 |
+
The output is variables plus a few base rules, because a theme that ships
|
| 924 |
+
opinionated component CSS stops being a theme and starts being a fork of
|
| 925 |
+
whatever it lands in.
|
| 926 |
+
"""
|
| 927 |
+
|
| 928 |
+
del skill
|
| 929 |
+
_known(arguments, frozenset({"name", "colors", "heading_font", "body_font", "filename"}))
|
| 930 |
+
theme_name = _line(arguments, "name", limit=60)
|
| 931 |
+
colors_text = _multiline(arguments, "colors", limit=1200)
|
| 932 |
+
heading = str(arguments.get("heading_font") or "Georgia, serif").strip()[:80]
|
| 933 |
+
body_font = str(arguments.get("body_font") or "Arial, sans-serif").strip()[:80]
|
| 934 |
+
variables = []
|
| 935 |
+
for line in colors_text.splitlines():
|
| 936 |
+
line = line.strip()
|
| 937 |
+
if not line:
|
| 938 |
+
continue
|
| 939 |
+
key, _, value = line.partition(":")
|
| 940 |
+
key = key.strip().lower()
|
| 941 |
+
value = value.strip()
|
| 942 |
+
if not re.fullmatch(r"[a-z][a-z0-9-]{0,30}", key):
|
| 943 |
+
raise SkillInputError(f"colour name {key!r} must be lowercase-with-hyphens")
|
| 944 |
+
if not re.fullmatch(r"#[0-9A-Fa-f]{3,8}", value):
|
| 945 |
+
raise SkillInputError(f"colour {key!r} needs a #hex value, got {value!r}")
|
| 946 |
+
variables.append(f" --{key}: {value};")
|
| 947 |
+
if not variables:
|
| 948 |
+
raise SkillInputError("colors needs at least one 'name: #hex' line")
|
| 949 |
+
sheet = (
|
| 950 |
+
f"/* Theme: {theme_name} β generated by distinct */\n"
|
| 951 |
+
":root {\n" + "\n".join(variables) + "\n"
|
| 952 |
+
f" --font-heading: {heading};\n"
|
| 953 |
+
f" --font-body: {body_font};\n"
|
| 954 |
+
"}\n\n"
|
| 955 |
+
"body { font-family: var(--font-body); }\n"
|
| 956 |
+
"h1, h2, h3, h4 { font-family: var(--font-heading); }\n"
|
| 957 |
+
)
|
| 958 |
+
return {
|
| 959 |
+
"artifact": text_artifact(
|
| 960 |
+
_filename(arguments.get("filename"), "theme.css", ".css"), "text/css", sheet
|
| 961 |
+
)
|
| 962 |
+
}
|
| 963 |
+
|
| 964 |
+
|
| 965 |
+
def tailored_guide(arguments: Mapping[str, object], skill: SkillDefinition) -> dict[str, object]:
|
| 966 |
+
"""The skill's own guide, applied to the user's situation.
|
| 967 |
+
|
| 968 |
+
The knowledge lives in the skill directory as ``assets/guide.md`` β data,
|
| 969 |
+
reviewed with the skill β and this handler fills its placeholders. One
|
| 970 |
+
handler therefore serves every adapted guidance skill, which is how a
|
| 971 |
+
library of upstream skills stays reviewable: each contributes text, none
|
| 972 |
+
contributes code.
|
| 973 |
+
"""
|
| 974 |
+
|
| 975 |
+
_known(arguments, frozenset({"subject", "context", "filename"}))
|
| 976 |
+
subject = _line(arguments, "subject", limit=200)
|
| 977 |
+
context = str(arguments.get("context") or "").strip()[:4000]
|
| 978 |
+
template = skill.resource("assets/guide.md")
|
| 979 |
+
text = render(
|
| 980 |
+
template,
|
| 981 |
+
{
|
| 982 |
+
"subject": subject,
|
| 983 |
+
"context": context or "No further context was given.",
|
| 984 |
+
"skill": skill.name,
|
| 985 |
+
},
|
| 986 |
+
)
|
| 987 |
+
default_name = f"{skill.name}.md"
|
| 988 |
+
return {
|
| 989 |
+
"artifact": text_artifact(
|
| 990 |
+
_filename(arguments.get("filename"), default_name, ".md"),
|
| 991 |
+
"text/markdown",
|
| 992 |
+
text,
|
| 993 |
+
)
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
|
| 997 |
#: The fixed handler table. A skill directory may name one of these and
|
| 998 |
#: nothing else.
|
| 999 |
HANDLERS: Mapping[str, object] = {
|
|
|
|
| 1001 |
"skill_scaffold": skill_scaffold,
|
| 1002 |
"review_checklist": review_checklist,
|
| 1003 |
"csv_table": csv_table,
|
| 1004 |
+
"document_docx": document_docx,
|
| 1005 |
+
"spreadsheet_xlsx": spreadsheet_xlsx,
|
| 1006 |
+
"deck_pptx": deck_pptx,
|
| 1007 |
+
"web_page": web_page,
|
| 1008 |
+
"theme_css": theme_css,
|
| 1009 |
+
"tailored_guide": tailored_guide,
|
| 1010 |
}
|
| 1011 |
|
| 1012 |
HANDLER_NAMES = frozenset(HANDLERS)
|
distinct_skills/manifest.py
CHANGED
|
@@ -44,18 +44,37 @@ PACKAGED_ROOT = Path(__file__).resolve().parent / "skills"
|
|
| 44 |
#: expect the review to ask why the digest changed.
|
| 45 |
PINNED_DIGESTS: Mapping[str, str] = MappingProxyType(
|
| 46 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
"csv-table": "ff87a44fe03901f0cfb61f46209b6cc7138acf07d87279c936cd8711b0a5e455",
|
|
|
|
|
|
|
|
|
|
| 48 |
"review-checklist": "98af818e3e4c178021bb87bcf355934b5effca7240b738f48ee344f7c6b6686f",
|
| 49 |
"skill-scaffold": "158d1d1ff1ff3ad83865537708cd4464cdd114f420912136917b9c143c32e18e",
|
|
|
|
| 50 |
"status-update": "48b7b1914de88b70e0ff9d19262987aca01696f6ebce061d0793543cbacc309e",
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
)
|
| 53 |
|
| 54 |
#: A bound on the repository itself, not on any one skill. Every member is a
|
| 55 |
#: line in the tool menu a small model has to choose from, and selection error
|
| 56 |
#: is the dominant failure below 7B, so growing the repository is a decision
|
| 57 |
-
#: with a measurable cost rather than free shelf space.
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
_SLUG_RE = re.compile(r"[^a-z0-9_-]+")
|
| 61 |
|
|
|
|
| 44 |
#: expect the review to ask why the digest changed.
|
| 45 |
PINNED_DIGESTS: Mapping[str, str] = MappingProxyType(
|
| 46 |
{
|
| 47 |
+
"academy-guide": "489ddd4c963552d243d68aa71af3e9f86ea58b6eb15fa77676eb21297d20e683",
|
| 48 |
+
"algorithmic-art": "a20eeb2e787e8f01f66fefc61fa34d84c7046015ba39728952f97ebe29709822",
|
| 49 |
+
"brand-guidelines": "bf9abe6506aa7785d36b027fd90f5a5552b40a36f1201ee09b3a680bbfff74d3",
|
| 50 |
+
"canvas-design": "776e112543645c5b22515c6e503ab572cde35c4633e0fd09e2d7c53ed912cde0",
|
| 51 |
+
"claude-api": "7f41a0e79797eb14c67e8a7fbfbabdb02da229829997496d719af141a597d0ce",
|
| 52 |
+
"create-deck": "e7c9a709f4609af0e5560544ecd8b05cb32fe5e52c147a23690f700700ec5b88",
|
| 53 |
+
"create-docx": "b1e950ab46f324bca84f6aade1d6772f13090e1fb1cb57df761c41bcb4764b7d",
|
| 54 |
+
"create-xlsx": "73325b2bd0d99ec781610ed9da1af59fef2655fb6e7b3766a29e2915d5ec449d",
|
| 55 |
"csv-table": "ff87a44fe03901f0cfb61f46209b6cc7138acf07d87279c936cd8711b0a5e455",
|
| 56 |
+
"discernment-nudge": "1901fbb6a6a0980fe3c8fc5297d492c39ef02f1c46bbcd77223cf7622cabcd5b",
|
| 57 |
+
"frontend-design": "283205e755626aae7c517f6a3b0f9b8d16b2d30f32d24822080b58393eae689a",
|
| 58 |
+
"mcp-builder": "fafc5acb2b8e408182ed8f2be49e9c1dcf41827ceca7bba6aacaccce3393b37b",
|
| 59 |
"review-checklist": "98af818e3e4c178021bb87bcf355934b5effca7240b738f48ee344f7c6b6686f",
|
| 60 |
"skill-scaffold": "158d1d1ff1ff3ad83865537708cd4464cdd114f420912136917b9c143c32e18e",
|
| 61 |
+
"slack-gif-creator": "854f079d2843b80cbf3bd261c099f187c07ad0edf69646ca122eee7e77007ae9",
|
| 62 |
"status-update": "48b7b1914de88b70e0ff9d19262987aca01696f6ebce061d0793543cbacc309e",
|
| 63 |
+
"theme-factory": "1b5d9800d869e902757d1b1733f98f3936319ee090035ffe9a82603662ccebe8",
|
| 64 |
+
"web-artifacts-builder": "4d0576368ca16dea4d5dea809b4766146d49ef84354269289843f55e76f9615e",
|
| 65 |
+
"webapp-testing": "cde6767ba5c2fd1cca6e86007010b228cb9296ffe3764485122ade9dd88c52fd",
|
| 66 |
}
|
| 67 |
)
|
| 68 |
|
| 69 |
#: A bound on the repository itself, not on any one skill. Every member is a
|
| 70 |
#: line in the tool menu a small model has to choose from, and selection error
|
| 71 |
#: is the dominant failure below 7B, so growing the repository is a decision
|
| 72 |
+
#: with a measurable cost rather than free shelf space. The bound was raised
|
| 73 |
+
#: from 16 when the adapted Anthropic skills arrived, because the cost moved:
|
| 74 |
+
#: a run's model only ever sees the members chosen for that run in the
|
| 75 |
+
#: composer, so the shelf can be larger than any one menu. The menu a model
|
| 76 |
+
#: reads is still the thing to keep small, per run, by choosing deliberately.
|
| 77 |
+
MAX_REPOSITORY_SKILLS = 24
|
| 78 |
|
| 79 |
_SLUG_RE = re.compile(r"[^a-z0-9_-]+")
|
| 80 |
|
distinct_skills/skills/academy-guide/SKILL.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: academy-guide
|
| 3 |
+
description: "Build a structured learning path for a topic: modules, order, exercises and checkpoints, delivered as a course outline. Arguments: subject (the topic), context (learner's starting point, time available, goal) and an optional filename. Adapted from Anthropic's academy-guide skill."
|
| 4 |
+
license: Apache-2.0. Adapted for distinct from the Anthropic skills repository.
|
| 5 |
+
source: https://github.com/anthropics/skills
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# Academy guide
|
| 9 |
+
|
| 10 |
+
## What this produces
|
| 11 |
+
|
| 12 |
+
One markdown document: the method below, applied to the subject the
|
| 13 |
+
caller names. The knowledge lives in `assets/guide.md` beside this
|
| 14 |
+
file; the handler substitutes the subject and context and nothing
|
| 15 |
+
else. Data, never code.
|
distinct_skills/skills/academy-guide/assets/guide.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Learning path: {{subject}}
|
| 2 |
+
|
| 3 |
+
{{context}}
|
| 4 |
+
|
| 5 |
+
## Method β adapted from Anthropic's academy-guide skill
|
| 6 |
+
|
| 7 |
+
1. **Start from the learner, not the syllabus.** State what they can already
|
| 8 |
+
do and the one capability the path exists to add.
|
| 9 |
+
2. **Modules of one idea each.** Five to eight modules, each answering a
|
| 10 |
+
single question, ordered so every module uses only what came before.
|
| 11 |
+
3. **Exercise before explanation length.** Each module gets one exercise a
|
| 12 |
+
learner completes in under thirty minutes; the explanation is only as
|
| 13 |
+
long as the exercise needs.
|
| 14 |
+
4. **Checkpoints that prove capability.** After every second module, a task
|
| 15 |
+
with no scaffolding. Passing it is the syllabus; the modules are merely
|
| 16 |
+
the route.
|
| 17 |
+
5. **A finishing project.** One realistic piece of work using the whole
|
| 18 |
+
path. Define what done looks like on day one.
|
| 19 |
+
|
| 20 |
+
## Produce
|
| 21 |
+
|
| 22 |
+
Write the module list with a one-line goal, a resource and an exercise for
|
| 23 |
+
each, the checkpoints, and the finishing project for the subject.
|
distinct_skills/skills/academy-guide/tool.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1",
|
| 3 |
+
"handler": "tailored_guide",
|
| 4 |
+
"input_schema": {
|
| 5 |
+
"type": "object",
|
| 6 |
+
"additionalProperties": false,
|
| 7 |
+
"properties": {
|
| 8 |
+
"subject": {
|
| 9 |
+
"type": "string",
|
| 10 |
+
"maxLength": 200,
|
| 11 |
+
"description": "what the guide should be applied to, in a few words"
|
| 12 |
+
},
|
| 13 |
+
"context": {
|
| 14 |
+
"type": "string",
|
| 15 |
+
"maxLength": 4000,
|
| 16 |
+
"description": "anything already known: audience, constraints, drafts, goals"
|
| 17 |
+
},
|
| 18 |
+
"filename": {
|
| 19 |
+
"type": "string",
|
| 20 |
+
"maxLength": 80,
|
| 21 |
+
"description": "optional file name for the result"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"required": [
|
| 25 |
+
"subject"
|
| 26 |
+
]
|
| 27 |
+
}
|
| 28 |
+
}
|
distinct_skills/skills/algorithmic-art/SKILL.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: algorithmic-art
|
| 3 |
+
description: "Plan a piece of algorithmic art: a named philosophy, a seeded system and the p5.js sketch structure, delivered as a document. Arguments: subject (theme or feeling), context (palette, constraints, interactivity wanted) and an optional filename. Adapted from Anthropic's algorithmic-art skill."
|
| 4 |
+
license: Apache-2.0. Adapted for distinct from the Anthropic skills repository.
|
| 5 |
+
source: https://github.com/anthropics/skills
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# Algorithmic art
|
| 9 |
+
|
| 10 |
+
## What this produces
|
| 11 |
+
|
| 12 |
+
One markdown document: the method below, applied to the subject the
|
| 13 |
+
caller names. The knowledge lives in `assets/guide.md` beside this
|
| 14 |
+
file; the handler substitutes the subject and context and nothing
|
| 15 |
+
else. Data, never code.
|
distinct_skills/skills/algorithmic-art/assets/guide.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Algorithmic art plan: {{subject}}
|
| 2 |
+
|
| 3 |
+
{{context}}
|
| 4 |
+
|
| 5 |
+
## Method β adapted from Anthropic's algorithmic-art skill
|
| 6 |
+
|
| 7 |
+
1. **Philosophy before pixels.** Write three sentences naming the idea the
|
| 8 |
+
piece explores β growth, erosion, interference, flocking β and the
|
| 9 |
+
feeling it should leave. Every later choice answers to these sentences.
|
| 10 |
+
2. **Seeded randomness.** One integer seed drives every random draw, so a
|
| 11 |
+
piece can be reproduced, varied and signed. State the seed in the corner
|
| 12 |
+
of the canvas.
|
| 13 |
+
3. **One system, explored.** Choose a single generative system (flow field,
|
| 14 |
+
subdivision, particle trails, reaction-diffusion) and push its
|
| 15 |
+
parameters, rather than layering three systems thinly.
|
| 16 |
+
4. **Parameter map.** Name 3-5 parameters, their ranges, and what each does
|
| 17 |
+
visually. These become the piece's interactive controls.
|
| 18 |
+
5. **Craft.** Margins, a considered palette of few colours, and density
|
| 19 |
+
tuned so the eye rests somewhere. Generative is not an excuse for noise.
|
| 20 |
+
|
| 21 |
+
## Produce
|
| 22 |
+
|
| 23 |
+
Write the philosophy, the seed, the system, the parameter map and the p5.js
|
| 24 |
+
sketch outline (setup, draw, the data structure) for the subject.
|
distinct_skills/skills/algorithmic-art/tool.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1",
|
| 3 |
+
"handler": "tailored_guide",
|
| 4 |
+
"input_schema": {
|
| 5 |
+
"type": "object",
|
| 6 |
+
"additionalProperties": false,
|
| 7 |
+
"properties": {
|
| 8 |
+
"subject": {
|
| 9 |
+
"type": "string",
|
| 10 |
+
"maxLength": 200,
|
| 11 |
+
"description": "what the guide should be applied to, in a few words"
|
| 12 |
+
},
|
| 13 |
+
"context": {
|
| 14 |
+
"type": "string",
|
| 15 |
+
"maxLength": 4000,
|
| 16 |
+
"description": "anything already known: audience, constraints, drafts, goals"
|
| 17 |
+
},
|
| 18 |
+
"filename": {
|
| 19 |
+
"type": "string",
|
| 20 |
+
"maxLength": 80,
|
| 21 |
+
"description": "optional file name for the result"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"required": [
|
| 25 |
+
"subject"
|
| 26 |
+
]
|
| 27 |
+
}
|
| 28 |
+
}
|
distinct_skills/skills/brand-guidelines/SKILL.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: brand-guidelines
|
| 3 |
+
description: "Apply a written brand system to whatever is being made: colours, type and usage rules as a one-page reference. Arguments: subject (what the brand should be applied to), context (the brand's colours, fonts and rules if known) and an optional filename. Adapted from Anthropic's brand-guidelines skill."
|
| 4 |
+
license: Apache-2.0. Adapted for distinct from the Anthropic skills repository.
|
| 5 |
+
source: https://github.com/anthropics/skills
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# Brand guidelines
|
| 9 |
+
|
| 10 |
+
## What this produces
|
| 11 |
+
|
| 12 |
+
One markdown document: the method below, applied to the subject the
|
| 13 |
+
caller names. The knowledge lives in `assets/guide.md` beside this
|
| 14 |
+
file; the handler substitutes the subject and context and nothing
|
| 15 |
+
else. Data, never code.
|
distinct_skills/skills/brand-guidelines/assets/guide.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Brand application: {{subject}}
|
| 2 |
+
|
| 3 |
+
{{context}}
|
| 4 |
+
|
| 5 |
+
## Method β adapted from Anthropic's brand-guidelines skill
|
| 6 |
+
|
| 7 |
+
1. **Palette.** Name the core colours with hex values and one line each on
|
| 8 |
+
where they belong (background, text, accent, warning). If the brand names
|
| 9 |
+
no palette, propose one and mark it proposed.
|
| 10 |
+
2. **Typography.** One heading face, one body face, and the fallback stack.
|
| 11 |
+
State where each is used and the smallest size allowed.
|
| 12 |
+
3. **Voice.** Three adjectives the writing should earn, with one sentence of
|
| 13 |
+
what each rules out.
|
| 14 |
+
4. **Usage rules.** The five rules that stop the brand drifting: contrast
|
| 15 |
+
minimums, logo clearance, what never sits on what.
|
| 16 |
+
5. **Applied to the subject.** For the subject named above, list the exact
|
| 17 |
+
colours, faces and rules that apply, ready to hand to whoever builds it.
|
distinct_skills/skills/brand-guidelines/tool.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1",
|
| 3 |
+
"handler": "tailored_guide",
|
| 4 |
+
"input_schema": {
|
| 5 |
+
"type": "object",
|
| 6 |
+
"additionalProperties": false,
|
| 7 |
+
"properties": {
|
| 8 |
+
"subject": {
|
| 9 |
+
"type": "string",
|
| 10 |
+
"maxLength": 200,
|
| 11 |
+
"description": "what the guide should be applied to, in a few words"
|
| 12 |
+
},
|
| 13 |
+
"context": {
|
| 14 |
+
"type": "string",
|
| 15 |
+
"maxLength": 4000,
|
| 16 |
+
"description": "anything already known: audience, constraints, drafts, goals"
|
| 17 |
+
},
|
| 18 |
+
"filename": {
|
| 19 |
+
"type": "string",
|
| 20 |
+
"maxLength": 80,
|
| 21 |
+
"description": "optional file name for the result"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"required": [
|
| 25 |
+
"subject"
|
| 26 |
+
]
|
| 27 |
+
}
|
| 28 |
+
}
|
distinct_skills/skills/canvas-design/SKILL.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: canvas-design
|
| 3 |
+
description: "Plan a poster or static visual design: philosophy, composition, palette and type, delivered as a design document. Arguments: subject (what the piece is for), context (format, audience, mood, copy) and an optional filename. Adapted from Anthropic's canvas-design skill."
|
| 4 |
+
license: Apache-2.0. Adapted for distinct from the Anthropic skills repository.
|
| 5 |
+
source: https://github.com/anthropics/skills
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# Canvas design
|
| 9 |
+
|
| 10 |
+
## What this produces
|
| 11 |
+
|
| 12 |
+
One markdown document: the method below, applied to the subject the
|
| 13 |
+
caller names. The knowledge lives in `assets/guide.md` beside this
|
| 14 |
+
file; the handler substitutes the subject and context and nothing
|
| 15 |
+
else. Data, never code.
|
distinct_skills/skills/canvas-design/assets/guide.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design plan: {{subject}}
|
| 2 |
+
|
| 3 |
+
{{context}}
|
| 4 |
+
|
| 5 |
+
## Method β adapted from Anthropic's canvas-design skill
|
| 6 |
+
|
| 7 |
+
1. **A visual philosophy in three sentences.** What the piece believes β
|
| 8 |
+
restraint, tension, warmth β written before any element is placed. If a
|
| 9 |
+
choice cannot be defended from these sentences, it goes.
|
| 10 |
+
2. **One focal point.** Decide what the eye lands on first and make every
|
| 11 |
+
other element quieter than it. Two focal points are none.
|
| 12 |
+
3. **Grid and margins.** Choose the canvas size, a simple grid, and margins
|
| 13 |
+
generous enough to be a design element themselves.
|
| 14 |
+
4. **Palette of few.** Two or three colours plus a neutral. Name the exact
|
| 15 |
+
values and what each is for.
|
| 16 |
+
5. **Type as hierarchy.** One display face, one text face, three sizes.
|
| 17 |
+
The headline earns its size; everything else steps down.
|
| 18 |
+
6. **The subtle reference.** One quiet nod to the subject β a shape, a
|
| 19 |
+
texture, an angle β that rewards a second look without shouting.
|
| 20 |
+
|
| 21 |
+
## Produce
|
| 22 |
+
|
| 23 |
+
Write the philosophy and then the concrete spec: canvas, grid, palette,
|
| 24 |
+
faces, sizes, and the placement of each element, ready to execute.
|