Repository navigation
Expand file tree
/
Copy pathapi_server.py
More file actions
294 lines (241 loc) Β· 10.8 KB
/
Copy pathapi_server.py
File metadata and controls
294 lines (241 loc) Β· 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
"""
api_server.py β Production-ready standalone API for PyCompat model
===================================================================
Start:
python api_server.py # defaults to ./model on port 8080
python api_server.py --port 9000 # custom port
python api_server.py --model ./my_model # custom model path
Use from any project:
import requests
r = requests.post("http://localhost:8080/api/predict", json={
"package": "boto3", "version": "1.42.49",
"python_version": "3.12", "platform": "darwin_x86_64"
})
print(r.json())
"""
import os
import sys
import json
import argparse
from flask import Flask, request, jsonify
from flask_cors import CORS
from pycompat_model import PyCompatModel
from validate import validate_install_code
from deploy_check import compare_local_vs_deploy
from observation_store import normalize_observation, append_observation, ObservationValidationError
app = Flask(__name__)
CORS(app)
model = None
@app.route("/api/predict", methods=["POST"])
def predict():
"""
Predict compatibility for a single package.
Request body:
{ "package": "boto3", "version": "1.42.49", "python_version": "3.12", "platform": "darwin_x86_64" }
Response:
{ "is_compatible": true, "confidence": 0.997, "predicted_error_type": "none", ... }
"""
data = request.get_json()
if not data:
return jsonify({"error": "JSON body required"}), 400
required = ["package", "version", "python_version"]
missing = [k for k in required if k not in data]
if missing:
return jsonify({"error": f"Missing fields: {missing}"}), 400
result = model.predict(
data["package"], data["version"],
data["python_version"], data.get("platform", "darwin_x86_64")
)
return jsonify(result)
@app.route("/api/predict/batch", methods=["POST"])
def predict_batch():
"""
Batch prediction.
Request body:
{ "queries": [
{ "package": "boto3", "version": "1.42.49", "python_version": "3.12" },
{ "package": "alembic", "version": "1.18.4", "python_version": "3.9" }
]}
Response:
{ "results": [...], "count": 2 }
"""
data = request.get_json()
queries = data.get("queries", [])
if not queries:
return jsonify({"error": "queries array required"}), 400
results = model.predict_batch(queries)
return jsonify({"results": results, "count": len(results)})
@app.route("/api/recommend", methods=["POST"])
def recommend():
"""
Get version recommendations.
Request body:
{ "package": "alembic", "python_version": "3.9", "top_n": 5 }
Response:
{ "recommendations": [...], "package": "alembic" }
"""
data = request.get_json()
if not data:
return jsonify({"error": "JSON body required"}), 400
pkg = data.get("package")
pyver = data.get("python_version")
if not pkg or not pyver:
return jsonify({"error": "package and python_version required"}), 400
recs = model.recommend(
pkg, pyver,
data.get("platform", "darwin_x86_64"),
data.get("top_n", 5)
)
return jsonify({"recommendations": recs, "package": pkg, "python_version": pyver})
@app.route("/api/validate", methods=["POST"])
def validate():
"""
Validate (and if needed, correct) LLM-generated `pip install` code -- the
same contract as the middleware repo's /api/validate-llm-code, built from
this repo's own ML model + live PyPI verification + joint dependency
checking. LLM calls are opt-in (see use_llm), same as Docker (docker_verify).
Request body:
{ "code": "pip install torch==2.8.0 torchvision==0.17.0",
"python_version": "3.12", "platform": "darwin_x86_64",
"live": true, "docker_verify": false, "use_llm": false }
docker_verify: when true, runs real `pip install` + `import` tests in
Docker containers for anything that looks clean from metadata alone
(linux_x86_64/linux_aarch64 targets only, requires a running Docker
daemon). Catches failures no metadata check can see -- e.g. a package
with a platform-universal wheel that transitively depends on
something OS-locked. Slow (5-15s per package); off by default.
use_llm: when true, consults an LLM (via Groq, needs GROQ_API_KEY set) for
any package outside the trained catalog, surfaced as a labeled,
unverified second opinion -- never as fact. Automatically forces real
Docker verification for that specific package too, regardless of
docker_verify: a package unknown to the ML model is exactly the case
that most needs real ground truth. Off by default.
Response:
{ "original_code": ..., "corrected_code": ..., "risk_score": ...,
"joint_dependency_conflicts": [...], "packages": [...] }
"""
data = request.get_json()
if not data or "code" not in data:
return jsonify({"error": "JSON body with 'code' required"}), 400
result = validate_install_code(
data["code"],
python_version=data.get("python_version", "3.12"),
platform=data.get("platform", "darwin_x86_64"),
model=model,
live=data.get("live", True),
docker_verify=data.get("docker_verify", False),
use_llm=data.get("use_llm", False),
)
return jsonify(result)
@app.route("/api/deploy-check", methods=["POST"])
def deploy_check():
"""
Compares dependencies that work on your local machine against a deployment
target (e.g. a Linux Docker container) and flags anything that would break
only after deploying -- the "works on my machine" gap.
Request body:
{ "code": "pip install torch==2.8.0", "python_version": "3.12",
"local_platform": "darwin_arm64", "deploy_platform": "linux_x86_64" }
Optional: "deploy_python_version" (if the deploy target pins a different
Python version than local), "live": false (ML-only), "docker_verify": true
(runs real `pip install` + `import` tests in Docker containers for the
deploy side -- linux targets only, requires a running Docker daemon,
slow at 5-15s/package, catches failures no metadata check can see),
"use_llm": true (consults an LLM for any deploy-side package outside
the trained catalog -- labeled, unverified second opinion, and
automatically forces real Docker verification for that package too;
needs GROQ_API_KEY set, off by default).
Response:
{ "safe_to_deploy": bool, "regressions": [...], "local": {...}, "deploy": {...} }
"""
data = request.get_json()
if not data or "code" not in data:
return jsonify({"error": "JSON body with 'code' required"}), 400
result = compare_local_vs_deploy(
data["code"],
python_version=data.get("python_version", "3.12"),
local_platform=data.get("local_platform", "darwin_x86_64"),
deploy_platform=data.get("deploy_platform", "linux_x86_64"),
deploy_python_version=data.get("deploy_python_version"),
model=model,
live=data.get("live", True),
docker_verify=data.get("docker_verify", False),
use_llm=data.get("use_llm", False),
)
return jsonify(result)
@app.route("/api/observations", methods=["POST"])
def observations():
"""
Accepts one real-world compatibility observation from any caller (a user's
machine, a CI run, another tool) -- a crowd-sourced supplement to data.json,
which today is a fixed curated snapshot. Append-only: this route NEVER
writes to data.json. It only appends to observations.jsonl; a submitted
row only reaches the trained model after promote_observations.py applies
corroboration checks (live PyPI existence + reputation/agreement) offline.
Request body:
{ "package": "torch", "version": "2.8.0", "python_version": "3.12",
"platform": "linux", "arch": "x86_64", "outcome": "fail",
"error_type": "build_error", "device_id_hash": "<opaque hash, not raw PII>",
"observed_via": "auto" }
"error_type" may be null (required-ish for "fail", ignored for "success").
"observed_via" must be "auto" -- this only accepts a result an agent
actually captured by running the install, never a self-reported opinion.
Response:
{ "accepted": true, "id": "...", "platform_key": "linux_x86_64" | null }
A null platform_key means the (platform, arch) combo doesn't map to any
platform this model has training data for -- the row is still stored,
but can never be promoted into data.json.
"""
data = request.get_json()
try:
record = normalize_observation(data or {})
except ObservationValidationError as e:
return jsonify({"error": str(e)}), 400
append_observation(record)
return jsonify({
"accepted": True,
"id": record["id"],
"platform_key": record["platform_key"],
}), 201
@app.route("/api/packages", methods=["GET"])
def packages():
"""List all known packages and their versions."""
return jsonify({
"packages": sorted(model.package_versions.keys()),
"count": len(model.package_versions)
})
@app.route("/api/info", methods=["GET"])
def info():
"""Model information and metadata."""
return jsonify({
"model_name": model.MODEL_NAME,
"model_version": model.MODEL_VERSION,
"metadata": model.metadata,
"total_packages": len(model.package_versions),
})
@app.route("/api/health", methods=["GET"])
def health():
return jsonify({"status": "ok", "model_loaded": model is not None})
def main():
global model
parser = argparse.ArgumentParser(description="PyCompat API Server")
parser.add_argument("--model", default="./model", help="Path to model directory")
parser.add_argument("--port", type=int, default=8080, help="Port to run on")
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
args = parser.parse_args()
print(f"π¦ Loading model from {args.model}...")
model = PyCompatModel.load(args.model)
print(f"\nπ PyCompat API running at http://localhost:{args.port}")
print(f"\n Endpoints:")
print(f" POST /api/predict β Single prediction")
print(f" POST /api/predict/batch β Batch predictions")
print(f" POST /api/recommend β Version recommendations")
print(f" POST /api/validate β Validate + auto-correct pip install code")
print(f" POST /api/deploy-check β Compare local vs. deployment-target compatibility")
print(f" POST /api/observations β Submit a real-world compatibility observation")
print(f" GET /api/packages β List packages")
print(f" GET /api/info β Model info")
print(f" GET /api/health β Health check\n")
app.run(host=args.host, port=args.port, debug=False)
if __name__ == "__main__":
main()