deploy Nous v15 update
This commit is contained in:
+27
-13
@@ -47,6 +47,11 @@ AI_BASE_URL = os.environ.get("AI_BASE_URL", "").strip().rstrip("/")
|
||||
AI_API_KEY = os.environ.get("AI_API_KEY", "").strip()
|
||||
AI_MODEL = os.environ.get("AI_MODEL", "").strip()
|
||||
AI_PROVIDER = os.environ.get("AI_PROVIDER", "custom").strip().lower()
|
||||
# Cloudflare Browser Integrity checks can reject Python's default
|
||||
# ``Python-urllib/<version>`` fingerprint before an OpenAI-compatible request
|
||||
# reaches the provider. Identify this application consistently on provider
|
||||
# requests instead of relying on that default.
|
||||
OUTBOUND_USER_AGENT = os.environ.get("OUTBOUND_USER_AGENT", "ProspectOS/2.0 (+personal-use)").strip() or "ProspectOS/2.0 (+personal-use)"
|
||||
NOUS_BASE_URL = "https://inference-api.nousresearch.com/v1"
|
||||
NOUS_FREE_MODELS = [
|
||||
{"id": "meituan/longcat-2.0:free", "label": "Meituan · LongCat 2.0", "free": True},
|
||||
@@ -455,9 +460,17 @@ def extract_gmaps_results(raw: Any) -> list[dict[str, Any]]:
|
||||
return output
|
||||
|
||||
|
||||
def http_json(method: str, url: str, payload: Any = None, headers: dict[str, str] | None = None, timeout: float = 20) -> Any:
|
||||
def _upstream_error(prefix: str, code: int, detail: str) -> str:
|
||||
"""Return an actionable, provider-specific upstream error string."""
|
||||
lowered = detail.lower()
|
||||
if code == 403 and ("error 1010" in lowered or "error_code\":1010" in lowered or "error code: 1010" in lowered):
|
||||
return f"{prefix}_cloudflare_1010:{detail}"
|
||||
return f"{prefix}_http_{code}:{detail}"
|
||||
|
||||
|
||||
def http_json(method: str, url: str, payload: Any = None, headers: dict[str, str] | None = None, timeout: float = 20, error_prefix: str = "upstream") -> Any:
|
||||
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||
request_headers = {"Accept": "application/json", **(headers or {})}
|
||||
request_headers = {"Accept": "application/json", "User-Agent": OUTBOUND_USER_AGENT, **(headers or {})}
|
||||
if data is not None:
|
||||
request_headers["Content-Type"] = "application/json"
|
||||
request = urllib.request.Request(url, data=data, headers=request_headers, method=method.upper())
|
||||
@@ -466,19 +479,19 @@ def http_json(method: str, url: str, payload: Any = None, headers: dict[str, str
|
||||
body = response.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")[:300]
|
||||
raise RuntimeError(f"gmaps_http_{exc.code}:{detail}") from exc
|
||||
raise RuntimeError(_upstream_error(error_prefix, int(exc.code), detail)) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(f"gmaps_unreachable:{exc.reason}") from exc
|
||||
raise RuntimeError(f"{error_prefix}_unreachable:{exc.reason}") from exc
|
||||
try:
|
||||
return json.loads(body or b"{}")
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError("gmaps_invalid_json") from exc
|
||||
|
||||
|
||||
def http_bytes(method: str, url: str, payload: Any = None, headers: dict[str, str] | None = None, timeout: float = 20) -> bytes:
|
||||
def http_bytes(method: str, url: str, payload: Any = None, headers: dict[str, str] | None = None, timeout: float = 20, error_prefix: str = "upstream") -> bytes:
|
||||
"""Make a small HTTP request where the response is not JSON (CSV download)."""
|
||||
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||
request_headers = {"Accept": "*/*", **(headers or {})}
|
||||
request_headers = {"Accept": "*/*", "User-Agent": OUTBOUND_USER_AGENT, **(headers or {})}
|
||||
if data is not None:
|
||||
request_headers["Content-Type"] = "application/json"
|
||||
request = urllib.request.Request(url, data=data, headers=request_headers, method=method.upper())
|
||||
@@ -487,9 +500,9 @@ def http_bytes(method: str, url: str, payload: Any = None, headers: dict[str, st
|
||||
return response.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")[:300]
|
||||
raise RuntimeError(f"gmaps_http_{exc.code}:{detail}") from exc
|
||||
raise RuntimeError(_upstream_error(error_prefix, int(exc.code), detail)) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(f"gmaps_unreachable:{exc.reason}") from exc
|
||||
raise RuntimeError(f"{error_prefix}_unreachable:{exc.reason}") from exc
|
||||
|
||||
|
||||
def ai_runtime_settings() -> dict[str, str]:
|
||||
@@ -516,7 +529,7 @@ def nous_models() -> list[dict[str, Any]]:
|
||||
if time.monotonic() - _nous_models_cached_at < 600:
|
||||
return _nous_models_cache
|
||||
try:
|
||||
response = http_json("GET", NOUS_BASE_URL + "/models", timeout=10)
|
||||
response = http_json("GET", NOUS_BASE_URL + "/models", timeout=10, error_prefix="ai")
|
||||
raw = response.get("data", response) if isinstance(response, dict) else response
|
||||
models: list[dict[str, Any]] = []
|
||||
if isinstance(raw, list):
|
||||
@@ -556,6 +569,7 @@ def download_standard_results(job_id: str) -> list[dict[str, Any]]:
|
||||
GMAPS_URL + "/api/v1/jobs/" + quote_plus(str(job_id)) + "/download",
|
||||
headers=gmaps_headers(),
|
||||
timeout=30,
|
||||
error_prefix="gmaps",
|
||||
)
|
||||
try:
|
||||
text = body.decode("utf-8-sig")
|
||||
@@ -594,7 +608,7 @@ def submit_gmaps_job(query: str, include_email: bool = False, extra_reviews: boo
|
||||
"extra_reviews": bool(extra_reviews),
|
||||
"max_time": max(60, min(3600, GMAPS_REQUEST_TIMEOUT)),
|
||||
}
|
||||
response = http_json("POST", GMAPS_URL + endpoint, payload, gmaps_headers(), timeout=30)
|
||||
response = http_json("POST", GMAPS_URL + endpoint, payload, gmaps_headers(), timeout=30, error_prefix="gmaps")
|
||||
job_id = field_value(response, "job_id", "id", "JobID", "ID")
|
||||
if not job_id:
|
||||
raise RuntimeError("gmaps_job_id_missing")
|
||||
@@ -611,7 +625,7 @@ def poll_gmaps_job(job_id: str, progress: Callable[[str, int, int], None] | None
|
||||
terminal_ok = {"ok", "completed", "succeeded", "success"}
|
||||
terminal_bad = {"failed", "cancelled", "canceled", "discarded"}
|
||||
while time.monotonic() < deadline:
|
||||
response = http_json("GET", GMAPS_URL + "/api/v1/jobs/" + quote_plus(str(job_id)), headers=gmaps_headers(), timeout=30)
|
||||
response = http_json("GET", GMAPS_URL + "/api/v1/jobs/" + quote_plus(str(job_id)), headers=gmaps_headers(), timeout=30, error_prefix="gmaps")
|
||||
status = str(field_value(response, "status", "Status") or "pending").lower()
|
||||
result_count = as_int(field_value(response, "result_count", "ResultCount"))
|
||||
if progress:
|
||||
@@ -1018,7 +1032,7 @@ def ai_generate_message(prospect: dict[str, Any], offer: str) -> dict[str, Any]:
|
||||
# requires strict JSON and the parser below validates the response.
|
||||
if runtime["provider"] != "nous":
|
||||
request_payload["response_format"] = {"type": "json_object"}
|
||||
result = http_json("POST", ai_base_url + "/chat/completions", request_payload, headers=headers, timeout=90)
|
||||
result = http_json("POST", ai_base_url + "/chat/completions", request_payload, headers=headers, timeout=90, error_prefix="ai")
|
||||
content = (((result.get("choices") or [{}])[0].get("message") or {}).get("content") or "") if isinstance(result, dict) else ""
|
||||
try:
|
||||
parsed = json.loads(content)
|
||||
@@ -1184,7 +1198,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if runtime["provider"] == "nous" and not runtime["api_key"]:
|
||||
return self.send_json(400, {"error": "nous_api_key_required_for_standalone_app"})
|
||||
headers = {"Authorization": "Bearer " + runtime["api_key"]} if runtime["api_key"] else {}
|
||||
result = http_json("POST", runtime["base_url"] + "/chat/completions", {"model": runtime["model"], "temperature": 0, "max_tokens": 8, "messages": [{"role": "user", "content": "Reply with exactly OK."}]}, headers=headers, timeout=30)
|
||||
result = http_json("POST", runtime["base_url"] + "/chat/completions", {"model": runtime["model"], "temperature": 0, "max_tokens": 8, "messages": [{"role": "user", "content": "Reply with exactly OK."}]}, headers=headers, timeout=30, error_prefix="ai")
|
||||
content = (((result.get("choices") or [{}])[0].get("message") or {}).get("content") or "") if isinstance(result, dict) else ""
|
||||
if not str(content).strip():
|
||||
return self.send_json(502, {"error": "ai_empty_response"})
|
||||
|
||||
Reference in New Issue
Block a user