Spaces:
Paused
Paused
File size: 14,458 Bytes
3dc792e 44e7d09 3dc792e 53e4b19 3dc792e 3ce1968 599ba2f 3dc792e 3ce1968 3dc792e 599ba2f 3ce1968 599ba2f 3ce1968 599ba2f 3ce1968 599ba2f 3ce1968 599ba2f 3ce1968 599ba2f 3ce1968 3dc792e 62971d3 3ce1968 44e7d09 53e4b19 3ce1968 44e7d09 53e4b19 44e7d09 5ed70ba 53e4b19 3ce1968 44e7d09 62971d3 3ce1968 62971d3 44e7d09 3ce1968 | 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 | """Thin Gradio surface over the authoritative JourneyTest HTTP/service runtime."""
from contextlib import asynccontextmanager
import json, os, time, urllib.error, urllib.request
import gradio as gr
from fastapi import FastAPI, Request
from fastapi.responses import Response
import httpx, uvicorn
API = os.environ.get("JOURNEYTEST_API_BASE", "http://127.0.0.1:7861")
def request(path, method="GET", body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, method=method, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=15) as response:
return response.status, json.load(response)
def normalize_secret(value):
if value is None:
return None
normalized = str(value).strip()
return normalized or None
def normalize_secrets(values):
return {key: normalized for key, value in values.items() if (normalized := normalize_secret(value))}
def run_journey(url, goal, max_steps, timeout, screenshots, trace, laya_mode, cognition_profile, hosted_verification, vision_enabled, terminal_verification, openai_key, anthropic_key, gemini_key, hosted_key, vision_key, spark_key, context, domains, confidence):
try: metadata = json.loads(context or "{}")
except json.JSONDecodeError as error: raise gr.Error(f"Invalid context JSON: {error}")
payload = {"url": url, "goal": goal, "maxSteps": int(max_steps), "timeoutMs": int(float(timeout) * 1000), "screenshots": screenshots, "trace": trace, "confidenceThreshold": float(confidence), "context": {**metadata, "layaMode": laya_mode}, "sessionCredentials": normalize_secrets({"OPENAI_API_KEY": openai_key, "ANTHROPIC_API_KEY": anthropic_key, "GEMINI_API_KEY": gemini_key, "LAYA_HOSTED_API_KEY": hosted_key, "LAYA_VISION_API_KEY": vision_key, "SPARK_OPENAI_API_KEY": spark_key}), "cognitionProfile": cognition_profile, "cognitionOptions": {"hostedVerification": hosted_verification, "vision": vision_enabled, "cloudTerminalVerification": terminal_verification}, "allowedDomains": [x.strip() for x in domains.split(",") if x.strip()]}
_, created = request("/api/v1/journeys", "POST", payload); journey_id = created["journey_id"]
while True:
status_code, result = request(f"/api/v1/journeys/{journey_id}/result")
if status_code == 200:
steps = result.get("steps", []); last = steps[-1] if steps else {}; decision = last.get("decision", {})
summary = {"journey_id": journey_id, "status": result["status"], "current_step": result["step_count"], "current_url": result["final_url"], "candidate_count": len(last.get("candidates", [])), "latest_operation": decision.get("operation"), "selected_target": decision.get("elementIndex"), "confidence": decision.get("confidence")}
screenshots_out = result.get("artifacts", {}).get("screenshots", [])
yield summary, result.get("events", []), screenshots_out[-1] if screenshots_out else None, result, result
return
yield {"journey_id": journey_id, "status": result.get("status", "running")}, [], None, {}, result
time.sleep(.75)
PROFILE_LABELS = {
"Local Laya + Cloud": "local-cloud",
"Local Laya + Spark + Cloud": "local-spark-cloud",
"Local + Hosted Laya + Cloud": "dual-laya-cloud",
"Local + Hosted Laya + Spark + Cloud": "dual-laya-spark-cloud",
}
try:
_, RUNTIME_INFO = request("/api/v1/info")
except Exception:
RUNTIME_INFO = {"cognition": {}}
COGNITION = RUNTIME_INFO.get("cognition") or {}
DEFAULT_PROFILE = COGNITION.get("cognitionProfile", "local-cloud")
IS_SPACE = (COGNITION.get("source") or {}).get("deployment") == "hugging-face"
def apply_settings(profile_label, hosted_verification, vision_enabled, terminal_verification, *secrets):
profile = PROFILE_LABELS[profile_label]
configured = [name for name, value in zip(["OpenAI", "Anthropic", "Gemini", "Hosted Laya", "Laya Vision", "Spark"], secrets) if normalize_secret(value)]
suffix = f" Session-only credentials supplied for: {', '.join(configured)}." if configured else ""
return profile, hosted_verification, vision_enabled, terminal_verification, f"Applied `{profile}` for this browser session.{suffix} Credentials are not echoed or written."
def save_settings(profile_label, hosted_enabled, hosted_url, vision_enabled, vision_url, spark_enabled, spark_url, spark_model, cloud_enabled, provider, openai_base_url, model, attempts, *secrets):
if IS_SPACE: return "Running on Hugging Face Space. To persist credentials: Space β Settings β Repository secrets."
keys = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "LAYA_HOSTED_API_KEY", "LAYA_VISION_API_KEY", "SPARK_OPENAI_API_KEY"]
body = {"COGNITION_PROFILE": PROFILE_LABELS[profile_label], "LAYA_HOSTED_ENABLED": str(hosted_enabled).lower(), "LAYA_HOSTED_BASE_URL": hosted_url or "", "LAYA_VISION_ENABLED": str(vision_enabled).lower(), "LAYA_VISION_BASE_URL": vision_url or "", "SPARK_REASONING_ENABLED": str(spark_enabled).lower(), "SPARK_OPENAI_BASE_URL": spark_url or "", "SPARK_MODEL": spark_model, "CLOUD_REASONING_ENABLED": str(cloud_enabled).lower(), "REASONING_PROVIDER": provider, "OPENAI_BASE_URL": openai_base_url or "", "OPENAI_MODEL": model, "CLOUD_MAX_ATTEMPTS": str(int(attempts))}
body.update(normalize_secrets(dict(zip(keys, secrets))))
_, result = request("/api/v1/settings/local", "POST", body); return result["message"]
def test_connections():
_, result = request("/api/v1/settings/test", "POST", {}); return result
def build_ui():
with gr.Blocks(title="JourneyTest") as demo:
gr.Markdown("# JourneyTest Developer UI\nAll runs use the shared JourneyService.")
with gr.Row():
url = gr.Textbox(label="Start URL")
goal = gr.Textbox(label="Goal", lines=3)
profile_state = gr.State(DEFAULT_PROFILE)
hosted_verify_state = gr.State(False)
vision_state = gr.State(False)
terminal_verify_state = gr.State(False)
with gr.Row():
max_steps = gr.Number(15, label="Max steps")
timeout = gr.Number(120, label="Timeout seconds")
screenshots = gr.Checkbox(True, label="Capture screenshots")
trace = gr.Checkbox(False, label="Capture trace")
mode = gr.Dropdown(["auto", "remote", "embedded"], value="auto", label="Laya mode")
with gr.Accordion("Advanced", open=False):
context = gr.Code("{}", language="json", label="Context JSON")
domains = gr.Textbox(label="Allowed domains (comma-separated)")
confidence = gr.Slider(0, 1, .15, label="Confidence threshold")
run = gr.Button("Run journey", variant="primary")
with gr.Accordion("β Settings", open=False):
gr.Markdown("## Cognition Profile")
default_label = next((label for label, value in PROFILE_LABELS.items() if value == DEFAULT_PROFILE), "Local Laya + Cloud")
profile_select = gr.Dropdown(list(PROFILE_LABELS), value=default_label, label="Profile")
with gr.Row():
hosted_verify = gr.Checkbox(False, label="Hosted Laya verification")
vision_toggle = gr.Checkbox(False, label="Laya Vision")
terminal_verify = gr.Checkbox(False, label="Cloud terminal verification")
gr.Markdown("## System 1")
with gr.Row():
hosted_enabled = gr.Checkbox(False, label="Hosted Laya enabled")
hosted_url = gr.Textbox(label="Hosted Laya base URL")
gr.Markdown("## Vision")
vision_url = gr.Textbox(label="Laya Vision base URL")
gr.Markdown("## Local System 2")
with gr.Row():
spark_enabled = gr.Checkbox(DEFAULT_PROFILE.endswith("spark-cloud"), label="Spark enabled")
spark_url = gr.Textbox(label="Spark OpenAI base URL")
spark_model = gr.Textbox("spark-x2.5-1.7b", label="Spark model")
gr.Markdown("## Cloud System 2")
with gr.Row():
cloud_enabled = gr.Checkbox(True, label="Cloud enabled")
provider = gr.Dropdown(["openai", "anthropic", "gemini"], value="openai", label="Provider protocol")
openai_base_url = gr.Textbox(label="OpenAI-compatible Base URL")
reasoning_model = gr.Textbox(label="Model ID")
gr.Markdown("Credentials are session-only unless **Save local configuration** is explicitly selected. Existing values are never loaded into these fields.")
with gr.Row():
openai_key = gr.Textbox(label="OpenAI β " + credential_status("openai"), type="password")
anthropic_key = gr.Textbox(label="Anthropic β " + credential_status("anthropic"), type="password")
gemini_key = gr.Textbox(label="Gemini β " + credential_status("gemini"), type="password")
with gr.Row():
hosted_key = gr.Textbox(label="Hosted Laya β " + credential_status("hostedLaya"), type="password")
vision_key = gr.Textbox(label="Laya Vision β " + credential_status("layaVision"), type="password")
spark_key = gr.Textbox(label="Spark auth β " + credential_status("spark"), type="password")
gr.Markdown("## Advanced / retries")
attempts = gr.Number(3, minimum=1, maximum=5, label="Cloud max attempts")
deployment = "Running on Hugging Face Space. Session changes are ephemeral. To persist a credential: Space β Settings β Repository secrets." if IS_SPACE else "Running on localhost. Save local configuration writes `.env.local` with mode 0600."
gr.Markdown("## Deployment\n" + deployment)
settings_status = gr.Markdown(json.dumps(COGNITION, indent=2))
with gr.Row():
apply_button = gr.Button("Apply for this session", variant="primary")
test_button = gr.Button("Test connections")
save_button = gr.Button("Save local configuration", visible=not IS_SPACE)
with gr.Row():
status = gr.JSON(label="Live status")
screenshot = gr.Image(label="Latest screenshot", type="filepath")
events = gr.JSON(label="Event feed")
result = gr.JSON(label="Result")
raw = gr.JSON(label="Raw JourneyResult JSON")
with gr.Tab("Developer / API"):
gr.Markdown(f"API base URL: `{API}`\n\nHealth: `{API}/api/v1/health` Β· Ready: `{API}/api/v1/ready`\n\n```bash\ncurl -X POST {API}/api/v1/journeys -H 'content-type: application/json' -d '{{\"url\":\"https://example.com\",\"goal\":\"Open pricing\"}}'\n```")
# Gradio 5 requires registration in a Blocks context. Re-enter only after
# every referenced component has been constructed.
with demo:
secret_inputs = [openai_key, anthropic_key, gemini_key, hosted_key, vision_key, spark_key]
run.click(run_journey, [url, goal, max_steps, timeout, screenshots, trace, mode, profile_state, hosted_verify_state, vision_state, terminal_verify_state, *secret_inputs, context, domains, confidence], [status, events, screenshot, result, raw])
apply_button.click(apply_settings, [profile_select, hosted_verify, vision_toggle, terminal_verify, *secret_inputs], [profile_state, hosted_verify_state, vision_state, terminal_verify_state, settings_status])
test_button.click(test_connections, [], settings_status)
save_button.click(save_settings, [profile_select, hosted_enabled, hosted_url, vision_toggle, vision_url, spark_enabled, spark_url, spark_model, cloud_enabled, provider, openai_base_url, reasoning_model, attempts, *secret_inputs], settings_status)
return demo
def credential_status(name):
value = (COGNITION.get("credentials") or {}).get(name)
return "configured β" if value is True or value == "configured" else "not configured"
def build_app():
demo = build_ui()
@asynccontextmanager
async def lifespan(app):
# One pool is safe to reuse; transport errors do not poison httpx's
# client. Explicit connect/read/write/pool limits keep failures
# distinguishable without extending journey execution timeouts.
app.state.upstream = httpx.AsyncClient(timeout=httpx.Timeout(connect=5, read=30, write=10, pool=5))
try:
yield
finally:
await app.state.upstream.aclose()
app = FastAPI(title="JourneyTest API Proxy", docs_url="/docs", lifespan=lifespan)
try:
_, public_openapi = request("/api/v1/openapi.json")
app.openapi = lambda: public_openapi
except Exception:
pass
@app.get("/health")
async def health_endpoint(incoming: Request):
return await proxy_request(incoming, f"{API}/health", timeout=10)
@app.get("/api-docs")
async def api_docs_endpoint(incoming: Request):
return await proxy_request(incoming, f"{API}/api-docs", timeout=10)
@app.api_route("/api/{path:path}", methods=["GET", "POST"])
async def api_proxy(path: str, incoming: Request):
return await proxy_request(incoming, f"{API}/api/{path}")
return gr.mount_gradio_app(app, demo, path="/"), demo
async def proxy_request(incoming, url, timeout=None):
client = incoming.app.state.upstream
try:
upstream = await client.request(incoming.method, url, content=await incoming.body(), headers={"content-type": incoming.headers.get("content-type", "application/json")}, timeout=timeout)
except httpx.TimeoutException:
return Response(json.dumps({"error": {"code": "UPSTREAM_TIMEOUT", "message": "JourneyTest API did not respond before the proxy deadline.", "retryable": True}}), status_code=504, media_type="application/json")
except httpx.RequestError:
return Response(json.dumps({"error": {"code": "UPSTREAM_UNAVAILABLE", "message": "JourneyTest API is temporarily unavailable.", "retryable": True}}), status_code=502, media_type="application/json")
headers = {key: value for key, value in upstream.headers.items() if key.lower() in {"content-type", "content-disposition", "cache-control"}}
return Response(upstream.content, status_code=upstream.status_code, headers=headers)
if __name__ == "__main__":
application, _demo = build_app()
uvicorn.run(application, host=os.environ.get("JOURNEYTEST_HOST", "0.0.0.0"), port=int(os.environ.get("JOURNEYTEST_PORT", "7860")))
|