create-windy 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +82 -2
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/embedding-server/src/southwind_embedding_server/app.py +27 -20
- package/template/apps/embedding-server/src/southwind_embedding_server/config.py +111 -4
- package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +13 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/pin_cli.py +62 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/readiness.py +259 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/readiness_contracts.py +221 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/readiness_http.py +91 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/run_service.py +23 -3
- package/template/apps/embedding-server/tests/test_config.py +13 -0
- package/template/apps/embedding-server/tests/test_readiness.py +202 -0
- package/template/apps/embedding-server/tests/test_run_http.py +76 -4
- package/template/apps/embedding-server/tests/test_run_service.py +3 -1
- package/template/apps/server/src/agent/drizzle-screenshot-intake-repository.integration.test.ts +3 -0
- package/template/apps/server/src/agent/run-facade.ts +2 -1
- package/template/apps/server/src/agent/run-route-screenshot-context.test.ts +13 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +2 -0
- package/template/apps/server/src/agent/screenshot-context-admission.test.ts +249 -0
- package/template/apps/server/src/agent/screenshot-context-attestation.ts +60 -0
- package/template/apps/server/src/agent/screenshot-intake-bootstrap.ts +2 -4
- package/template/apps/server/src/agent/screenshot-intake-test-support.ts +4 -0
- package/template/apps/server/src/agent/screenshot-intake-v2-validation.ts +24 -0
- package/template/apps/server/src/agent/screenshot-intake-v2.test.ts +7 -0
- package/template/apps/server/src/agent/screenshot-intake-v2.ts +8 -10
- package/template/apps/server/src/agent/screenshot-run-binding.ts +17 -2
- package/template/apps/server/src/agent/vision-readiness-pin.ts +6 -48
- package/template/apps/server/src/ai-runtime-readiness/deployment-pin.ts +69 -0
- package/template/apps/server/src/embedding/contracts.ts +6 -1
- package/template/apps/server/src/embedding/facade-readiness.test.ts +307 -0
- package/template/apps/server/src/embedding/facade.ts +8 -6
- package/template/apps/server/src/embedding/readiness-admission.ts +26 -0
- package/template/apps/server/src/embedding/remote-runtime.test.ts +85 -0
- package/template/apps/server/src/embedding/remote-runtime.ts +21 -0
- package/template/apps/server/src/embedding/run-fingerprint.test.ts +26 -0
- package/template/apps/server/src/embedding/run-fingerprint.ts +11 -2
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +72 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +43 -0
- package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +186 -0
- package/template/docker-compose.yml +11 -0
- package/template/docs/architecture/ai-runtime-readiness.md +6 -0
- package/template/docs/platform/vision-ai-runtime.md +22 -1
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/index.ts +1 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-operation-composition.ts +2 -0
- package/template/packages/modules/src/ai-screenshot-context.test.ts +72 -0
- package/template/packages/modules/src/ai-screenshot-context.ts +50 -0
- package/template/packages/modules/src/manifest.ts +8 -0
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/screenshot-context-digest.test.ts +46 -0
- package/template/packages/shared/src/screenshot-context-digest.ts +72 -0
package/dist/cli.js
CHANGED
|
@@ -378,7 +378,9 @@ var optionalModuleManifests = [
|
|
|
378
378
|
"packages/modules/src/system-agent-tools.ts",
|
|
379
379
|
"packages/database/src/agent-runtime-schema.test.ts",
|
|
380
380
|
"packages/database/src/ai-output-artifacts.test.ts",
|
|
381
|
-
"packages/shared/src/ai-runtime-report.ts"
|
|
381
|
+
"packages/shared/src/ai-runtime-report.ts",
|
|
382
|
+
"packages/shared/src/screenshot-context-digest.ts",
|
|
383
|
+
"packages/shared/src/screenshot-context-digest.test.ts"
|
|
382
384
|
],
|
|
383
385
|
documentation: [
|
|
384
386
|
"docs/architecture/ai-runtime.md",
|
|
@@ -3092,6 +3094,9 @@ var agentComposeService = ` tool-sandbox-init:
|
|
|
3092
3094
|
EMBEDDING_ADAPTER_URL: \${EMBEDDING_ADAPTER_URL:-}
|
|
3093
3095
|
EMBEDDING_ADAPTER_TOKEN: \${EMBEDDING_ADAPTER_TOKEN:-}
|
|
3094
3096
|
EMBEDDING_DEPLOYMENT_KEY: \${EMBEDDING_DEPLOYMENT_KEY:-}
|
|
3097
|
+
EMBEDDING_DEPLOYMENT_IDENTITY: \${EMBEDDING_DEPLOYMENT_IDENTITY:-}
|
|
3098
|
+
EMBEDDING_RELEASE_DIGEST: \${EMBEDDING_RELEASE_DIGEST:-}
|
|
3099
|
+
EMBEDDING_OFFERS_DIGEST: \${EMBEDDING_OFFERS_DIGEST:-}
|
|
3095
3100
|
EMBEDDING_DEPLOYMENT_ALIAS: \${EMBEDDING_DEPLOYMENT_ALIAS:-}
|
|
3096
3101
|
EMBEDDING_MODEL: \${EMBEDDING_MODEL:-}
|
|
3097
3102
|
EMBEDDING_MODEL_VERSION: \${EMBEDDING_MODEL_VERSION:-}
|
|
@@ -3099,8 +3104,16 @@ var agentComposeService = ` tool-sandbox-init:
|
|
|
3099
3104
|
EMBEDDING_NORMALIZED: \${EMBEDDING_NORMALIZED:-true}
|
|
3100
3105
|
EMBEDDING_CAPABILITIES: \${EMBEDDING_CAPABILITIES:-embedding.text}
|
|
3101
3106
|
EMBEDDING_MAX_UNITS: \${EMBEDDING_MAX_UNITS:-256}
|
|
3107
|
+
EMBEDDING_MAX_DURATION_SECONDS: \${EMBEDDING_MAX_DURATION_SECONDS:-600}
|
|
3108
|
+
EMBEDDING_MAX_INPUT_BYTES: \${EMBEDDING_MAX_INPUT_BYTES:-268435456}
|
|
3109
|
+
EMBEDDING_MAX_OUTPUT_BYTES: \${EMBEDDING_MAX_OUTPUT_BYTES:-268435456}
|
|
3110
|
+
EMBEDDING_MAX_ARTIFACTS: \${EMBEDDING_MAX_ARTIFACTS:-16}
|
|
3111
|
+
EMBEDDING_MAX_PIXELS_PER_ARTIFACT: \${EMBEDDING_MAX_PIXELS_PER_ARTIFACT:-100000000}
|
|
3102
3112
|
EMBEDDING_RUN_ENCRYPTION_SECRET: \${EMBEDDING_RUN_ENCRYPTION_SECRET:-}
|
|
3103
3113
|
EMBEDDING_MAX_CONCURRENT_RUNS: \${EMBEDDING_MAX_CONCURRENT_RUNS:-1}
|
|
3114
|
+
EMBEDDING_MAX_QUEUE_DEPTH: \${EMBEDDING_MAX_QUEUE_DEPTH:-0}
|
|
3115
|
+
EMBEDDING_READINESS_TIMEOUT_SECONDS: \${EMBEDDING_READINESS_TIMEOUT_SECONDS:-5}
|
|
3116
|
+
EMBEDDING_READINESS_TTL_SECONDS: \${EMBEDDING_READINESS_TTL_SECONDS:-60}
|
|
3104
3117
|
EMBEDDING_RUN_TERMINAL_TTL_SECONDS: \${EMBEDDING_RUN_TERMINAL_TTL_SECONDS:-604800}
|
|
3105
3118
|
volumes:
|
|
3106
3119
|
- embedding-run-data:/app/data/embedding
|
|
@@ -3190,6 +3203,9 @@ EMBEDDING_RUN_ARTIFACT_URL=
|
|
|
3190
3203
|
EMBEDDING_ADAPTER_URL=
|
|
3191
3204
|
EMBEDDING_ADAPTER_TOKEN=
|
|
3192
3205
|
EMBEDDING_DEPLOYMENT_KEY=
|
|
3206
|
+
EMBEDDING_DEPLOYMENT_IDENTITY=
|
|
3207
|
+
EMBEDDING_RELEASE_DIGEST=
|
|
3208
|
+
EMBEDDING_OFFERS_DIGEST=
|
|
3193
3209
|
EMBEDDING_DEPLOYMENT_ALIAS=
|
|
3194
3210
|
EMBEDDING_MODEL=
|
|
3195
3211
|
EMBEDDING_MODEL_VERSION=
|
|
@@ -3197,8 +3213,16 @@ EMBEDDING_DIMENSIONS=
|
|
|
3197
3213
|
# EMBEDDING_NORMALIZED=true
|
|
3198
3214
|
# EMBEDDING_CAPABILITIES=embedding.text,embedding.image
|
|
3199
3215
|
# EMBEDDING_MAX_UNITS=256
|
|
3216
|
+
# EMBEDDING_MAX_DURATION_SECONDS=600
|
|
3217
|
+
# EMBEDDING_MAX_INPUT_BYTES=268435456
|
|
3218
|
+
# EMBEDDING_MAX_OUTPUT_BYTES=268435456
|
|
3219
|
+
# EMBEDDING_MAX_ARTIFACTS=16
|
|
3220
|
+
# EMBEDDING_MAX_PIXELS_PER_ARTIFACT=100000000
|
|
3200
3221
|
EMBEDDING_RUN_ENCRYPTION_SECRET=
|
|
3201
3222
|
# EMBEDDING_MAX_CONCURRENT_RUNS=1
|
|
3223
|
+
# EMBEDDING_MAX_QUEUE_DEPTH=0
|
|
3224
|
+
# EMBEDDING_READINESS_TIMEOUT_SECONDS=5
|
|
3225
|
+
# EMBEDDING_READINESS_TTL_SECONDS=60
|
|
3202
3226
|
# EMBEDDING_RUN_TERMINAL_TTL_SECONDS=604800
|
|
3203
3227
|
QWEN_API_URL=
|
|
3204
3228
|
QWEN_API_KEY=
|
|
@@ -7723,6 +7747,11 @@ var recipes = [
|
|
|
7723
7747
|
id: "starter-0.5.2-to-0.6.0",
|
|
7724
7748
|
from: "0.5.2",
|
|
7725
7749
|
to: "0.6.0"
|
|
7750
|
+
},
|
|
7751
|
+
{
|
|
7752
|
+
id: "starter-0.6.0-to-0.6.1",
|
|
7753
|
+
from: "0.6.0",
|
|
7754
|
+
to: "0.6.1"
|
|
7726
7755
|
}
|
|
7727
7756
|
];
|
|
7728
7757
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
@@ -8774,7 +8803,7 @@ async function mergeTextFile(base, ours, theirs) {
|
|
|
8774
8803
|
}
|
|
8775
8804
|
return { content: new TextEncoder().encode(result2.stdout) };
|
|
8776
8805
|
}
|
|
8777
|
-
if (result2.code
|
|
8806
|
+
if (result2.code > 0 && result2.code <= 127) {
|
|
8778
8807
|
const resolved = resolveConservativeConflicts(result2.stdout);
|
|
8779
8808
|
return resolved ? { content: new TextEncoder().encode(resolved) } : { conflict: "\u4E09\u65B9\u5408\u5E76\u5B58\u5728\u91CD\u53E0\u4FEE\u6539" };
|
|
8780
8809
|
}
|
|
@@ -8957,6 +8986,11 @@ function mergeValue(base, ours, theirs, path2, conflicts) {
|
|
|
8957
8986
|
if (files)
|
|
8958
8987
|
return files;
|
|
8959
8988
|
}
|
|
8989
|
+
if (path2.startsWith("package.json.scripts.")) {
|
|
8990
|
+
const script = mergeConjunctionScript(base, ours, theirs);
|
|
8991
|
+
if (script !== undefined)
|
|
8992
|
+
return script;
|
|
8993
|
+
}
|
|
8960
8994
|
if (isObject(base) && isObject(ours) && isObject(theirs)) {
|
|
8961
8995
|
const output = {};
|
|
8962
8996
|
const keys = new Set([
|
|
@@ -8983,6 +9017,52 @@ function mergeAppendOnlyStringSet(base, ours, theirs) {
|
|
|
8983
9017
|
}
|
|
8984
9018
|
return [...new Set([...ours, ...theirs])].sort();
|
|
8985
9019
|
}
|
|
9020
|
+
function mergeConjunctionScript(base, ours, theirs) {
|
|
9021
|
+
if (typeof base !== "string" || typeof ours !== "string" || typeof theirs !== "string") {
|
|
9022
|
+
return;
|
|
9023
|
+
}
|
|
9024
|
+
const baseCommands = conjunctionCommands(base);
|
|
9025
|
+
const oursCommands = conjunctionCommands(ours);
|
|
9026
|
+
const theirsCommands = conjunctionCommands(theirs);
|
|
9027
|
+
if (!baseCommands || !oursCommands || !theirsCommands || !unique(baseCommands) || !unique(oursCommands) || !unique(theirsCommands)) {
|
|
9028
|
+
return;
|
|
9029
|
+
}
|
|
9030
|
+
const oursGaps = additionsByBaseGap(baseCommands, oursCommands);
|
|
9031
|
+
const theirsGaps = additionsByBaseGap(baseCommands, theirsCommands);
|
|
9032
|
+
if (!oursGaps || !theirsGaps)
|
|
9033
|
+
return;
|
|
9034
|
+
const merged = [];
|
|
9035
|
+
for (let gap = 0;gap <= baseCommands.length; gap += 1) {
|
|
9036
|
+
const oursAdded = oursGaps[gap] ?? [];
|
|
9037
|
+
const theirsAdded = theirsGaps[gap] ?? [];
|
|
9038
|
+
if (oursAdded.length && theirsAdded.length && !equal(oursAdded, theirsAdded)) {
|
|
9039
|
+
return;
|
|
9040
|
+
}
|
|
9041
|
+
merged.push(...oursAdded.length ? oursAdded : theirsAdded);
|
|
9042
|
+
if (gap < baseCommands.length)
|
|
9043
|
+
merged.push(baseCommands[gap] ?? "");
|
|
9044
|
+
}
|
|
9045
|
+
return merged.join(" && ");
|
|
9046
|
+
}
|
|
9047
|
+
function conjunctionCommands(value) {
|
|
9048
|
+
const commands = value.split(" && ");
|
|
9049
|
+
return commands.length && commands.every((command) => command.length > 0) && commands.join(" && ") === value ? commands : undefined;
|
|
9050
|
+
}
|
|
9051
|
+
function additionsByBaseGap(base, branch) {
|
|
9052
|
+
const gaps = Array.from({ length: base.length + 1 }, () => []);
|
|
9053
|
+
let baseIndex = 0;
|
|
9054
|
+
for (const command of branch) {
|
|
9055
|
+
if (command === base[baseIndex]) {
|
|
9056
|
+
baseIndex += 1;
|
|
9057
|
+
} else {
|
|
9058
|
+
gaps[baseIndex]?.push(command);
|
|
9059
|
+
}
|
|
9060
|
+
}
|
|
9061
|
+
return baseIndex === base.length ? gaps : undefined;
|
|
9062
|
+
}
|
|
9063
|
+
function unique(values) {
|
|
9064
|
+
return new Set(values).size === values.length;
|
|
9065
|
+
}
|
|
8986
9066
|
function isStringArray(value) {
|
|
8987
9067
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
8988
9068
|
}
|
package/package.json
CHANGED
|
@@ -15,23 +15,33 @@ from .adapter import EmbeddingAdapter
|
|
|
15
15
|
from .config import EmbeddingConfig
|
|
16
16
|
from .contracts import EmbeddingRequest, EmbeddingResponse, ErrorResponse
|
|
17
17
|
from .error import SafeEmbeddingError
|
|
18
|
+
from .readiness import EmbeddingReadinessPort, EmbeddingReadinessProbe
|
|
18
19
|
from .run_cipher import EmbeddingRunCipher
|
|
19
|
-
from .run_service import EmbeddingRunService
|
|
20
|
+
from .run_service import EmbeddingExecutionPort, EmbeddingRunService
|
|
20
21
|
from .run_store import EmbeddingRunStore, RunConflictError
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def create_app(
|
|
24
25
|
config: EmbeddingConfig,
|
|
25
|
-
adapter:
|
|
26
|
+
adapter: EmbeddingExecutionPort | None = None,
|
|
27
|
+
readiness_probe: EmbeddingReadinessPort | None = None,
|
|
26
28
|
) -> FastAPI:
|
|
27
29
|
app = FastAPI(title="Southwind Embedding Runtime", docs_url=None, redoc_url=None)
|
|
28
30
|
runtime = adapter or EmbeddingAdapter(config)
|
|
31
|
+
readiness = readiness_probe or EmbeddingReadinessProbe(config)
|
|
29
32
|
store = EmbeddingRunStore(
|
|
30
33
|
config.run_store_path,
|
|
31
34
|
EmbeddingRunCipher(config.run_encryption_secret),
|
|
32
35
|
config.terminal_ttl_seconds,
|
|
33
36
|
)
|
|
34
|
-
|
|
37
|
+
|
|
38
|
+
async def admit(request: EmbeddingRequest) -> bool:
|
|
39
|
+
inspection = await readiness.inspect()
|
|
40
|
+
return readiness.supports(inspection, request)
|
|
41
|
+
|
|
42
|
+
service = EmbeddingRunService(
|
|
43
|
+
runtime, store, config.max_concurrent_runs, admit=admit
|
|
44
|
+
)
|
|
35
45
|
|
|
36
46
|
@asynccontextmanager
|
|
37
47
|
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
|
@@ -47,24 +57,9 @@ def create_app(
|
|
|
47
57
|
|
|
48
58
|
@app.get("/health/ready")
|
|
49
59
|
async def ready(_: None = Depends(authorize)) -> JSONResponse:
|
|
50
|
-
|
|
51
|
-
return JSONResponse(
|
|
52
|
-
{"status": "unavailable", "capabilities": []},
|
|
53
|
-
status_code=503,
|
|
54
|
-
)
|
|
60
|
+
inspection = await readiness.inspect()
|
|
55
61
|
return JSONResponse(
|
|
56
|
-
|
|
57
|
-
"status": "ready",
|
|
58
|
-
"capabilities": list(config.capabilities),
|
|
59
|
-
"limits": {"maxUnits": config.max_units},
|
|
60
|
-
"deployment": {
|
|
61
|
-
"alias": config.deployment_alias,
|
|
62
|
-
"model": config.model,
|
|
63
|
-
"version": config.version,
|
|
64
|
-
"dimensions": config.dimensions,
|
|
65
|
-
"normalized": config.normalized,
|
|
66
|
-
},
|
|
67
|
-
}
|
|
62
|
+
inspection.snapshot, status_code=200 if inspection.ready else 503
|
|
68
63
|
)
|
|
69
64
|
|
|
70
65
|
@app.post("/api/embeddings")
|
|
@@ -75,6 +70,12 @@ def create_app(
|
|
|
75
70
|
) -> EmbeddingResponse:
|
|
76
71
|
if not idempotency_key.strip() or len(idempotency_key) > 160:
|
|
77
72
|
raise HTTPException(status_code=400, detail="INVALID_INPUT")
|
|
73
|
+
if not await admit(request):
|
|
74
|
+
raise SafeEmbeddingError(
|
|
75
|
+
"CAPABILITY_UNAVAILABLE",
|
|
76
|
+
"Embedding Runtime 当前没有匹配的原子 offer",
|
|
77
|
+
True,
|
|
78
|
+
)
|
|
78
79
|
try:
|
|
79
80
|
async with asyncio.timeout(request.limits.max_duration_seconds):
|
|
80
81
|
return await runtime.embed(request)
|
|
@@ -98,6 +99,12 @@ def create_app(
|
|
|
98
99
|
or not valid_run_id(run_id)
|
|
99
100
|
):
|
|
100
101
|
raise HTTPException(status_code=400, detail="INVALID_INPUT")
|
|
102
|
+
if not await admit(request):
|
|
103
|
+
raise SafeEmbeddingError(
|
|
104
|
+
"CAPABILITY_UNAVAILABLE",
|
|
105
|
+
"Embedding Runtime 当前没有匹配的原子 offer",
|
|
106
|
+
True,
|
|
107
|
+
)
|
|
101
108
|
try:
|
|
102
109
|
record = await service.start(request, idempotency_key, run_id)
|
|
103
110
|
except RunConflictError:
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ipaddress
|
|
4
|
+
import re
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from urllib.parse import urlparse
|
|
6
7
|
|
|
8
|
+
_STABLE_KEY = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$")
|
|
9
|
+
_DEPLOYMENT_IDENTITY = re.compile(
|
|
10
|
+
r"^windy-deployment-v1_[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-"
|
|
11
|
+
r"[89ab][a-f0-9]{3}-[a-f0-9]{12}$"
|
|
12
|
+
)
|
|
13
|
+
_DIGEST = re.compile(r"^sha256:[a-f0-9]{64}$")
|
|
14
|
+
|
|
7
15
|
|
|
8
16
|
@dataclass(frozen=True)
|
|
9
17
|
class EmbeddingConfig:
|
|
@@ -19,14 +27,35 @@ class EmbeddingConfig:
|
|
|
19
27
|
dimensions: int
|
|
20
28
|
normalized: bool
|
|
21
29
|
capabilities: tuple[str, ...]
|
|
30
|
+
deployment_identity: str | None = None
|
|
31
|
+
release_digest: str | None = None
|
|
32
|
+
offers_digest: str | None = None
|
|
22
33
|
max_units: int = 256
|
|
34
|
+
max_duration_seconds: int = 600
|
|
35
|
+
max_input_bytes: int = 268_435_456
|
|
36
|
+
max_output_bytes: int = 268_435_456
|
|
37
|
+
max_artifacts: int = 16
|
|
38
|
+
max_pixels_per_artifact: int = 100_000_000
|
|
23
39
|
connect_timeout_seconds: float = 5
|
|
24
40
|
request_timeout_seconds: float = 120
|
|
25
41
|
run_store_path: str = "/app/data/embedding/runs.sqlite3"
|
|
26
42
|
run_encryption_secret: str = ""
|
|
27
43
|
max_concurrent_runs: int = 1
|
|
44
|
+
max_queue_depth: int = 0
|
|
45
|
+
readiness_timeout_seconds: float = 5
|
|
46
|
+
readiness_ttl_seconds: int = 60
|
|
28
47
|
terminal_ttl_seconds: int = 7 * 24 * 60 * 60
|
|
29
48
|
|
|
49
|
+
@property
|
|
50
|
+
def readiness_configured(self) -> bool:
|
|
51
|
+
return bool(
|
|
52
|
+
self.adapter_url
|
|
53
|
+
and self.deployment_key
|
|
54
|
+
and self.deployment_identity
|
|
55
|
+
and self.release_digest
|
|
56
|
+
and self.offers_digest
|
|
57
|
+
)
|
|
58
|
+
|
|
30
59
|
@classmethod
|
|
31
60
|
def from_environment(cls, environment: dict[str, str]) -> EmbeddingConfig:
|
|
32
61
|
artifact_url = environment.get(
|
|
@@ -53,8 +82,18 @@ class EmbeddingConfig:
|
|
|
53
82
|
adapter_url=adapter_url,
|
|
54
83
|
adapter_token=environment.get("EMBEDDING_ADAPTER_TOKEN", "").strip()
|
|
55
84
|
or None,
|
|
56
|
-
deployment_key=
|
|
57
|
-
|
|
85
|
+
deployment_key=optional_match(
|
|
86
|
+
environment, "EMBEDDING_DEPLOYMENT_KEY", _STABLE_KEY
|
|
87
|
+
),
|
|
88
|
+
deployment_identity=optional_match(
|
|
89
|
+
environment, "EMBEDDING_DEPLOYMENT_IDENTITY", _DEPLOYMENT_IDENTITY
|
|
90
|
+
),
|
|
91
|
+
release_digest=optional_match(
|
|
92
|
+
environment, "EMBEDDING_RELEASE_DIGEST", _DIGEST
|
|
93
|
+
),
|
|
94
|
+
offers_digest=optional_match(
|
|
95
|
+
environment, "EMBEDDING_OFFERS_DIGEST", _DIGEST
|
|
96
|
+
),
|
|
58
97
|
deployment_alias=required(environment, "EMBEDDING_DEPLOYMENT_ALIAS"),
|
|
59
98
|
model=required(environment, "EMBEDDING_MODEL"),
|
|
60
99
|
version=required(environment, "EMBEDDING_MODEL_VERSION"),
|
|
@@ -75,6 +114,19 @@ class EmbeddingConfig:
|
|
|
75
114
|
)
|
|
76
115
|
),
|
|
77
116
|
max_units=positive_int(environment, "EMBEDDING_MAX_UNITS", 256),
|
|
117
|
+
max_duration_seconds=positive_int(
|
|
118
|
+
environment, "EMBEDDING_MAX_DURATION_SECONDS", 600
|
|
119
|
+
),
|
|
120
|
+
max_input_bytes=positive_int(
|
|
121
|
+
environment, "EMBEDDING_MAX_INPUT_BYTES", 268_435_456
|
|
122
|
+
),
|
|
123
|
+
max_output_bytes=positive_int(
|
|
124
|
+
environment, "EMBEDDING_MAX_OUTPUT_BYTES", 268_435_456
|
|
125
|
+
),
|
|
126
|
+
max_artifacts=positive_int(environment, "EMBEDDING_MAX_ARTIFACTS", 16),
|
|
127
|
+
max_pixels_per_artifact=positive_int(
|
|
128
|
+
environment, "EMBEDDING_MAX_PIXELS_PER_ARTIFACT", 100_000_000
|
|
129
|
+
),
|
|
78
130
|
run_store_path=environment.get(
|
|
79
131
|
"EMBEDDING_RUN_STORE_PATH", "/app/data/embedding/runs.sqlite3"
|
|
80
132
|
).strip(),
|
|
@@ -84,6 +136,15 @@ class EmbeddingConfig:
|
|
|
84
136
|
max_concurrent_runs=positive_int(
|
|
85
137
|
environment, "EMBEDDING_MAX_CONCURRENT_RUNS", 1
|
|
86
138
|
),
|
|
139
|
+
max_queue_depth=non_negative_int(
|
|
140
|
+
environment, "EMBEDDING_MAX_QUEUE_DEPTH", 0
|
|
141
|
+
),
|
|
142
|
+
readiness_timeout_seconds=bounded_positive_float(
|
|
143
|
+
environment, "EMBEDDING_READINESS_TIMEOUT_SECONDS", 5, 30
|
|
144
|
+
),
|
|
145
|
+
readiness_ttl_seconds=bounded_positive_int(
|
|
146
|
+
environment, "EMBEDDING_READINESS_TTL_SECONDS", 60, 300
|
|
147
|
+
),
|
|
87
148
|
terminal_ttl_seconds=positive_int(
|
|
88
149
|
environment, "EMBEDDING_RUN_TERMINAL_TTL_SECONDS", 7 * 24 * 60 * 60
|
|
89
150
|
),
|
|
@@ -120,8 +181,54 @@ def boolean(environment: dict[str, str], key: str, default: bool) -> bool:
|
|
|
120
181
|
|
|
121
182
|
|
|
122
183
|
def required_capabilities(value: tuple[str, ...]) -> tuple[str, ...]:
|
|
123
|
-
if
|
|
124
|
-
|
|
184
|
+
if (
|
|
185
|
+
not value
|
|
186
|
+
or len(value) != len(set(value))
|
|
187
|
+
or any(not _STABLE_KEY.fullmatch(item) for item in value)
|
|
188
|
+
):
|
|
189
|
+
raise ValueError("EMBEDDING_CAPABILITIES 无效")
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def optional_match(
|
|
194
|
+
environment: dict[str, str], key: str, pattern: re.Pattern[str]
|
|
195
|
+
) -> str | None:
|
|
196
|
+
value = environment.get(key, "").strip()
|
|
197
|
+
if value and not pattern.fullmatch(value):
|
|
198
|
+
raise ValueError(f"{key} 格式无效")
|
|
199
|
+
return value or None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def non_negative_int(environment: dict[str, str], key: str, default: int) -> int:
|
|
203
|
+
raw = environment.get(key, str(default)).strip()
|
|
204
|
+
try:
|
|
205
|
+
value = int(raw)
|
|
206
|
+
except ValueError:
|
|
207
|
+
raise ValueError(f"{key} 必须是非负整数") from None
|
|
208
|
+
if value < 0:
|
|
209
|
+
raise ValueError(f"{key} 必须是非负整数")
|
|
210
|
+
return value
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def bounded_positive_int(
|
|
214
|
+
environment: dict[str, str], key: str, default: int, maximum: int
|
|
215
|
+
) -> int:
|
|
216
|
+
value = positive_int(environment, key, default)
|
|
217
|
+
if value > maximum:
|
|
218
|
+
raise ValueError(f"{key} 不能超过 {maximum}")
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def bounded_positive_float(
|
|
223
|
+
environment: dict[str, str], key: str, default: float, maximum: float
|
|
224
|
+
) -> float:
|
|
225
|
+
raw = environment.get(key, str(default)).strip()
|
|
226
|
+
try:
|
|
227
|
+
value = float(raw)
|
|
228
|
+
except ValueError:
|
|
229
|
+
raise ValueError(f"{key} 必须是正数") from None
|
|
230
|
+
if value <= 0 or value > maximum:
|
|
231
|
+
raise ValueError(f"{key} 必须大于 0 且不超过 {maximum:g}")
|
|
125
232
|
return value
|
|
126
233
|
|
|
127
234
|
|
|
@@ -66,6 +66,18 @@ class ProviderDataPolicy(CanonicalModel):
|
|
|
66
66
|
envelope_fields: list[dict[str, JsonValue]]
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
class ReadinessPinV1(CanonicalModel):
|
|
70
|
+
key: str = Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$")
|
|
71
|
+
identity: str = Field(
|
|
72
|
+
pattern=(
|
|
73
|
+
r"^windy-deployment-v1_[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-"
|
|
74
|
+
r"[89ab][a-f0-9]{3}-[a-f0-9]{12}$"
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
release_digest: str = Field(pattern=r"^sha256:[a-f0-9]{64}$")
|
|
78
|
+
offers_digest: str = Field(pattern=r"^sha256:[a-f0-9]{64}$")
|
|
79
|
+
|
|
80
|
+
|
|
69
81
|
class EmbeddingRequest(CanonicalModel):
|
|
70
82
|
kind: Literal["embedding"]
|
|
71
83
|
operation_key: str = Field(min_length=1)
|
|
@@ -77,6 +89,7 @@ class EmbeddingRequest(CanonicalModel):
|
|
|
77
89
|
limits: RuntimeLimits
|
|
78
90
|
provider_egress: ProviderEgress
|
|
79
91
|
provider_data_policy: ProviderDataPolicy | None = None
|
|
92
|
+
readiness_pin: ReadinessPinV1 | None = None
|
|
80
93
|
|
|
81
94
|
|
|
82
95
|
class EmbeddingVector(CanonicalModel):
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .config import EmbeddingConfig
|
|
10
|
+
from .readiness_contracts import (
|
|
11
|
+
AdapterEmbeddingReadinessV1,
|
|
12
|
+
compute_offers_digest,
|
|
13
|
+
compute_release_digest,
|
|
14
|
+
final_offers,
|
|
15
|
+
)
|
|
16
|
+
from .readiness_http import get_json
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def compute_pins(
|
|
20
|
+
config: EmbeddingConfig,
|
|
21
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
22
|
+
) -> dict[str, str]:
|
|
23
|
+
if not config.deployment_key or not config.deployment_identity:
|
|
24
|
+
raise ValueError("Embedding deployment key/identity 缺失")
|
|
25
|
+
if not config.adapter_url:
|
|
26
|
+
raise ValueError("Embedding Adapter 未配置")
|
|
27
|
+
adapter = AdapterEmbeddingReadinessV1.model_validate(
|
|
28
|
+
await get_json(
|
|
29
|
+
f"{config.adapter_url.rstrip('/')}/v1/embedding/readiness",
|
|
30
|
+
config.adapter_token,
|
|
31
|
+
config.readiness_timeout_seconds,
|
|
32
|
+
transport,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
deployment = adapter.deployment
|
|
36
|
+
if (
|
|
37
|
+
deployment.model != config.model
|
|
38
|
+
or deployment.version != config.version
|
|
39
|
+
or deployment.dimensions != config.dimensions
|
|
40
|
+
or deployment.normalized != config.normalized
|
|
41
|
+
):
|
|
42
|
+
raise ValueError("Embedding Adapter deployment 漂移")
|
|
43
|
+
offers = final_offers(config, adapter)
|
|
44
|
+
return {
|
|
45
|
+
"deploymentKey": config.deployment_key,
|
|
46
|
+
"deploymentIdentity": config.deployment_identity,
|
|
47
|
+
"releaseDigest": compute_release_digest(adapter),
|
|
48
|
+
"offersDigest": compute_offers_digest(offers),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def main() -> None:
|
|
53
|
+
try:
|
|
54
|
+
pins = await compute_pins(EmbeddingConfig.from_environment(dict(os.environ)))
|
|
55
|
+
except Exception as error:
|
|
56
|
+
message = "Embedding readiness full probe 未通过,未写入 pins"
|
|
57
|
+
raise SystemExit(message) from error
|
|
58
|
+
print(json.dumps(pins, separators=(",", ":"), sort_keys=True))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
asyncio.run(main())
|