deploy Nous v22 update
This commit is contained in:
+146
-27
@@ -73,17 +73,71 @@ _schema_lock = threading.Lock()
|
||||
_schema_path: str | None = None
|
||||
|
||||
|
||||
def _saved_settings() -> dict[str, str]:
|
||||
"""Read persisted runtime settings, falling back cleanly during startup."""
|
||||
try:
|
||||
conn = db()
|
||||
try:
|
||||
return {str(row["key"]): str(row["value"]) for row in conn.execute("SELECT key,value FROM settings")}
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _setting_int(saved: dict[str, str], key: str, env_key: str, default: int, minimum: int, maximum: int) -> int:
|
||||
try:
|
||||
value = int(saved.get(key, os.environ.get(env_key, str(default))))
|
||||
except (TypeError, ValueError):
|
||||
value = default
|
||||
return max(minimum, min(maximum, value))
|
||||
|
||||
|
||||
def _setting_float(saved: dict[str, str], key: str, env_key: str, default: float, minimum: float, maximum: float) -> float:
|
||||
try:
|
||||
value = float(saved.get(key, os.environ.get(env_key, str(default))))
|
||||
except (TypeError, ValueError):
|
||||
value = default
|
||||
return max(minimum, min(maximum, value))
|
||||
|
||||
|
||||
def _setting_bool(saved: dict[str, str], key: str, env_key: str, default: bool) -> bool:
|
||||
raw = str(saved.get(key, os.environ.get(env_key, "true" if default else "false"))).strip().lower()
|
||||
return raw not in {"0", "false", "no", "off", "disabled"}
|
||||
|
||||
|
||||
def browser_use_settings() -> dict[str, Any]:
|
||||
"""Return the live Browser Use controls from Settings or .env defaults."""
|
||||
saved = _saved_settings()
|
||||
return {
|
||||
"enabled": _setting_bool(saved, "BROWSER_USE_ENABLED", "BROWSER_USE_ENABLED", BROWSER_USE_ENABLED),
|
||||
"max_steps": _setting_int(saved, "BROWSER_USE_MAX_STEPS", "BROWSER_USE_MAX_STEPS", 10, 3, 50),
|
||||
"timeout_seconds": _setting_int(saved, "BROWSER_USE_TIMEOUT_SECONDS", "BROWSER_USE_TIMEOUT_SECONDS", 600, 30, 1800),
|
||||
"llm_timeout_seconds": _setting_int(saved, "BROWSER_USE_LLM_TIMEOUT_SECONDS", "BROWSER_USE_LLM_TIMEOUT_SECONDS", 90, 10, 300),
|
||||
"headless": True,
|
||||
"public_only": True,
|
||||
}
|
||||
|
||||
|
||||
def browser_use_limits() -> tuple[int, int]:
|
||||
"""Return bounded Browser Use steps/timeout even if .env is malformed."""
|
||||
try:
|
||||
steps = int(os.environ.get("BROWSER_USE_MAX_STEPS", "10"))
|
||||
except (TypeError, ValueError):
|
||||
steps = 10
|
||||
try:
|
||||
timeout = int(os.environ.get("BROWSER_USE_TIMEOUT_SECONDS", "120"))
|
||||
except (TypeError, ValueError):
|
||||
timeout = 120
|
||||
return max(3, min(15, steps)), max(30, min(300, timeout))
|
||||
"""Return bounded Browser Use steps/timeout for compatibility with callers."""
|
||||
runtime = browser_use_settings()
|
||||
return int(runtime["max_steps"]), int(runtime["timeout_seconds"])
|
||||
|
||||
|
||||
def gmaps_runtime_settings() -> dict[str, Any]:
|
||||
"""Return live Maps worker timing controls from Settings or .env defaults."""
|
||||
saved = _saved_settings()
|
||||
return {
|
||||
"request_timeout_seconds": _setting_int(saved, "GMAPS_REQUEST_TIMEOUT", "GMAPS_REQUEST_TIMEOUT", GMAPS_REQUEST_TIMEOUT, 60, 7200),
|
||||
"poll_seconds": _setting_float(saved, "GMAPS_POLL_SECONDS", "GMAPS_POLL_SECONDS", GMAPS_POLL_SECONDS, 0.5, 60.0),
|
||||
"max_depth": _setting_int(saved, "GMAPS_MAX_DEPTH", "GMAPS_MAX_DEPTH", 1, 1, 10),
|
||||
}
|
||||
|
||||
|
||||
def website_scan_timeout_seconds() -> int:
|
||||
saved = _saved_settings()
|
||||
return _setting_int(saved, "WEBSITE_SCAN_TIMEOUT", "WEBSITE_SCAN_TIMEOUT", 8, 2, 60)
|
||||
|
||||
|
||||
def now() -> str:
|
||||
@@ -730,7 +784,9 @@ def submit_gmaps_job(query: str, include_email: bool = False, extra_reviews: boo
|
||||
raise RuntimeError("gmaps_not_configured")
|
||||
include_email = True
|
||||
lang = os.environ.get("GMAPS_LANG", "en")
|
||||
depth = max(1, min(10, int(os.environ.get("GMAPS_MAX_DEPTH", "1"))))
|
||||
maps_runtime = gmaps_runtime_settings()
|
||||
depth = int(maps_runtime["max_depth"])
|
||||
request_timeout = int(maps_runtime["request_timeout_seconds"])
|
||||
if GMAPS_API_MODE == "saas":
|
||||
endpoint = "/api/v1/scrape"
|
||||
payload = {
|
||||
@@ -739,7 +795,7 @@ def submit_gmaps_job(query: str, include_email: bool = False, extra_reviews: boo
|
||||
"max_depth": depth,
|
||||
"email": bool(include_email),
|
||||
"extra_reviews": bool(extra_reviews),
|
||||
"timeout": max(1, min(300, GMAPS_REQUEST_TIMEOUT)),
|
||||
"timeout": max(1, min(7200, request_timeout)),
|
||||
}
|
||||
else:
|
||||
# Standard gosom web mode uses the local job API. It has no API-key
|
||||
@@ -753,7 +809,7 @@ def submit_gmaps_job(query: str, include_email: bool = False, extra_reviews: boo
|
||||
"depth": depth,
|
||||
"email": bool(include_email),
|
||||
"extra_reviews": bool(extra_reviews),
|
||||
"max_time": max(60, min(3600, GMAPS_REQUEST_TIMEOUT)),
|
||||
"max_time": max(60, min(7200, request_timeout)),
|
||||
}
|
||||
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")
|
||||
@@ -765,7 +821,9 @@ def submit_gmaps_job(query: str, include_email: bool = False, extra_reviews: boo
|
||||
def poll_gmaps_job(job_id: str, progress: Callable[[str, int, int], None] | None = None) -> list[dict[str, Any]]:
|
||||
if not GMAPS_URL:
|
||||
raise RuntimeError("gmaps_not_configured")
|
||||
deadline = time.monotonic() + max(60, GMAPS_REQUEST_TIMEOUT + 60)
|
||||
maps_runtime = gmaps_runtime_settings()
|
||||
deadline = time.monotonic() + max(60, int(maps_runtime["request_timeout_seconds"]) + 60)
|
||||
poll_seconds = float(maps_runtime["poll_seconds"])
|
||||
if GMAPS_API_MODE == "saas":
|
||||
terminal_ok = {"completed", "succeeded", "success"}
|
||||
else:
|
||||
@@ -786,7 +844,7 @@ def poll_gmaps_job(job_id: str, progress: Callable[[str, int, int], None] | None
|
||||
if status in terminal_bad:
|
||||
error = field_value(response, "error", "Error")
|
||||
raise RuntimeError("gmaps_job_" + status + (":" + as_text(error) if error else ""))
|
||||
time.sleep(max(0.5, GMAPS_POLL_SECONDS))
|
||||
time.sleep(poll_seconds)
|
||||
# The worker can finish writing its CSV just after its status endpoint
|
||||
# exceeds our polling window. Recover a completed/partial result when it
|
||||
# is available rather than discarding the run as a false failure.
|
||||
@@ -995,9 +1053,9 @@ def enrich_place(item: dict[str, Any]) -> dict[str, Any]:
|
||||
if item.get("website"):
|
||||
scan_started = time.monotonic()
|
||||
record_log("search", "website_scan_started", prospect_id=as_int(item.get("id")) or None,
|
||||
details={"business": item.get("name"), "url": _safe_log_url(str(item.get("website")))})
|
||||
details={"business": item.get("name"), "url": _safe_log_url(str(item.get("website"))), "timeout_seconds": website_scan_timeout_seconds()})
|
||||
try:
|
||||
item.update(scan_website(str(item["website"])))
|
||||
item.update(scan_website(str(item["website"]), timeout_seconds=website_scan_timeout_seconds()))
|
||||
record_log("search", "website_scan_completed", prospect_id=as_int(item.get("id")) or None,
|
||||
duration_ms=int((time.monotonic() - scan_started) * 1000),
|
||||
details={"business": item.get("name"), "url": _safe_log_url(str(item.get("website"))), "status": item.get("website_status"), "emails_found": len(item.get("website_emails") or [])})
|
||||
@@ -1388,6 +1446,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
auth_required = GMAPS_API_MODE == "saas"
|
||||
ready = backend_mode() == "browser" or (bool(GMAPS_URL) and (not auth_required or bool(GMAPS_API_KEY)))
|
||||
ai_runtime = ai_runtime_settings()
|
||||
browser_runtime = browser_use_settings()
|
||||
return self.send_json(200, {
|
||||
"backend": backend_mode(),
|
||||
"gmaps_url": bool(GMAPS_URL),
|
||||
@@ -1397,7 +1456,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"ready": ready,
|
||||
"email_extraction_default": True,
|
||||
"email_extraction_always": True,
|
||||
"browser_use_enabled": BROWSER_USE_ENABLED,
|
||||
"browser_use_enabled": browser_runtime["enabled"],
|
||||
"ai_provider": ai_runtime["provider"],
|
||||
"ai_configured": bool(ai_runtime["base_url"] and ai_runtime["model"] and (ai_runtime["provider"] != "nous" or ai_runtime["api_key"])),
|
||||
})
|
||||
@@ -1442,8 +1501,20 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if parsed.path == "/api/settings":
|
||||
ai_runtime = ai_runtime_settings()
|
||||
models = nous_models() if ai_runtime["provider"] == "nous" else []
|
||||
browser_steps, browser_timeout = browser_use_limits()
|
||||
return self.send_json(200, {"maps": {"backend": backend_mode(), "worker_url": GMAPS_URL, "ready": backend_mode() == "browser" or bool(GMAPS_URL)}, "ai": {"provider": ai_runtime["provider"], "base_url": ai_runtime["base_url"], "model": ai_runtime["model"], "configured": bool(ai_runtime["base_url"] and ai_runtime["model"] and (ai_runtime["provider"] != "nous" or ai_runtime["api_key"])), "models": models}, "browser_use": {"enabled": BROWSER_USE_ENABLED, "headless": True, "public_only": True, "max_steps": browser_steps, "timeout_seconds": browser_timeout}, "email_collection": {"always_on": True, "public_only": True}, "limits": {"max_results": MAX_RESULTS, "website_scan_timeout_seconds": 8}})
|
||||
browser_runtime = browser_use_settings()
|
||||
maps_runtime = gmaps_runtime_settings()
|
||||
scan_timeout = website_scan_timeout_seconds()
|
||||
return self.send_json(200, {
|
||||
"maps": {
|
||||
"backend": backend_mode(), "worker_url": GMAPS_URL,
|
||||
"ready": backend_mode() == "browser" or bool(GMAPS_URL),
|
||||
**maps_runtime,
|
||||
},
|
||||
"ai": {"provider": ai_runtime["provider"], "base_url": ai_runtime["base_url"], "model": ai_runtime["model"], "configured": bool(ai_runtime["base_url"] and ai_runtime["model"] and (ai_runtime["provider"] != "nous" or ai_runtime["api_key"])), "models": models},
|
||||
"browser_use": browser_runtime,
|
||||
"email_collection": {"always_on": True, "public_only": True},
|
||||
"limits": {"max_results": MAX_RESULTS, "website_scan_timeout_seconds": scan_timeout},
|
||||
})
|
||||
if parsed.path == "/api/prospects":
|
||||
params = parse_qs(parsed.query)
|
||||
q = (params.get("q", [""])[0]).strip().lower()
|
||||
@@ -1546,15 +1617,58 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return self.send_json(400, {"error": str(exc)})
|
||||
if self.path == "/api/settings":
|
||||
try:
|
||||
payload = self.read_json(); ai = payload.get("ai") if isinstance(payload.get("ai"), dict) else {}
|
||||
payload = self.read_json()
|
||||
ai = payload.get("ai") if isinstance(payload.get("ai"), dict) else {}
|
||||
browser = payload.get("browser_use") if isinstance(payload.get("browser_use"), dict) else {}
|
||||
maps = payload.get("maps") if isinstance(payload.get("maps"), dict) else {}
|
||||
limits = payload.get("limits") if isinstance(payload.get("limits"), dict) else {}
|
||||
conn = db()
|
||||
saved_keys: list[str] = []
|
||||
|
||||
def save_setting(key: str, value: Any) -> None:
|
||||
text_value = str(value).strip()[:500]
|
||||
conn.execute("INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at", (key, text_value, now()))
|
||||
saved_keys.append(key)
|
||||
|
||||
for key in ("AI_PROVIDER", "AI_BASE_URL", "AI_API_KEY", "AI_MODEL"):
|
||||
if key in ai:
|
||||
value = str(ai.get(key) or "").strip()[:500]
|
||||
conn.execute("INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at", (key, value, now()))
|
||||
conn.commit(); saved={str(r["key"]):str(r["value"]) for r in conn.execute("SELECT key,value FROM settings")}; conn.close()
|
||||
# An empty password means “keep the existing credential”
|
||||
# so a normal Settings save cannot erase a working key.
|
||||
if key == "AI_API_KEY" and not str(ai.get(key) or "").strip():
|
||||
continue
|
||||
save_setting(key, ai.get(key) or "")
|
||||
|
||||
if "enabled" in browser:
|
||||
enabled_value = str(browser.get("enabled") or "").strip().lower() not in {"0", "false", "no", "off", "disabled"}
|
||||
save_setting("BROWSER_USE_ENABLED", "true" if enabled_value else "false")
|
||||
if "max_steps" in browser:
|
||||
save_setting("BROWSER_USE_MAX_STEPS", max(3, min(50, int(browser.get("max_steps")))))
|
||||
if "timeout_seconds" in browser:
|
||||
save_setting("BROWSER_USE_TIMEOUT_SECONDS", max(30, min(1800, int(browser.get("timeout_seconds")))))
|
||||
if "llm_timeout_seconds" in browser:
|
||||
save_setting("BROWSER_USE_LLM_TIMEOUT_SECONDS", max(10, min(300, int(browser.get("llm_timeout_seconds")))))
|
||||
|
||||
if "request_timeout_seconds" in maps:
|
||||
save_setting("GMAPS_REQUEST_TIMEOUT", max(60, min(7200, int(maps.get("request_timeout_seconds")))))
|
||||
if "poll_seconds" in maps:
|
||||
save_setting("GMAPS_POLL_SECONDS", max(0.5, min(60.0, float(maps.get("poll_seconds")))))
|
||||
if "max_depth" in maps:
|
||||
save_setting("GMAPS_MAX_DEPTH", max(1, min(10, int(maps.get("max_depth")))))
|
||||
if "website_scan_timeout_seconds" in limits:
|
||||
save_setting("WEBSITE_SCAN_TIMEOUT", max(2, min(60, int(limits.get("website_scan_timeout_seconds")))))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
record_log("action", "settings_updated", details={"keys": saved_keys})
|
||||
ai_runtime = ai_runtime_settings()
|
||||
return self.send_json(200, {"saved": True, "ai": {"provider": ai_runtime["provider"], "base_url": ai_runtime["base_url"], "model": ai_runtime["model"], "configured": bool(ai_runtime["base_url"] and ai_runtime["model"] and (ai_runtime["provider"] != "nous" or ai_runtime["api_key"]))}})
|
||||
return self.send_json(200, {
|
||||
"saved": True,
|
||||
"saved_keys": saved_keys,
|
||||
"ai": {"provider": ai_runtime["provider"], "base_url": ai_runtime["base_url"], "model": ai_runtime["model"], "configured": bool(ai_runtime["base_url"] and ai_runtime["model"] and (ai_runtime["provider"] != "nous" or ai_runtime["api_key"]))},
|
||||
"browser_use": browser_use_settings(),
|
||||
"maps": gmaps_runtime_settings(),
|
||||
"limits": {"website_scan_timeout_seconds": website_scan_timeout_seconds()},
|
||||
})
|
||||
except Exception as exc:
|
||||
return self.send_json(400, {"error": str(exc)})
|
||||
if urlparse(self.path).path == "/api/ai/test":
|
||||
@@ -1584,12 +1698,15 @@ class Handler(BaseHTTPRequestHandler):
|
||||
conn.close()
|
||||
if not row:
|
||||
return self.send_json(404, {"error": "not_found"})
|
||||
if not BROWSER_USE_ENABLED:
|
||||
browser_runtime = browser_use_settings()
|
||||
if not browser_runtime["enabled"]:
|
||||
return self.send_json(503, {"error": "browser_use_disabled"})
|
||||
try:
|
||||
runtime = ai_runtime_settings()
|
||||
record_log("browser", "website_investigation_started", prospect_id=prospect_id,
|
||||
details={"business": row["name"], "model": runtime["model"], "public_only": True})
|
||||
details={"business": row["name"], "model": runtime["model"], "public_only": True,
|
||||
"max_steps": browser_runtime["max_steps"], "timeout_seconds": browser_runtime["timeout_seconds"],
|
||||
"llm_timeout_seconds": browser_runtime["llm_timeout_seconds"]})
|
||||
|
||||
def browser_log(kind: str, event: str, details: dict[str, Any]) -> None:
|
||||
record_log(kind, event, prospect_id=prospect_id, details=details)
|
||||
@@ -1597,6 +1714,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
candidates = investigate_websites(
|
||||
dict(row), base_url=runtime["base_url"], api_key=runtime["api_key"],
|
||||
model=runtime["model"], user_agent=OUTBOUND_USER_AGENT, log_callback=browser_log,
|
||||
enabled=browser_runtime["enabled"], max_steps=browser_runtime["max_steps"],
|
||||
timeout_seconds=browser_runtime["timeout_seconds"], llm_timeout_seconds=browser_runtime["llm_timeout_seconds"],
|
||||
)
|
||||
record_log("browser", "website_investigation_completed", prospect_id=prospect_id,
|
||||
details={"candidate_count": len(candidates), "model": runtime["model"]})
|
||||
|
||||
Reference in New Issue
Block a user