diff --git a/.a2a/agent.dsl.json b/.a2a/agent.dsl.json index ad720ed..847f6d2 100644 --- a/.a2a/agent.dsl.json +++ b/.a2a/agent.dsl.json @@ -3,7 +3,7 @@ "language": "python", "name": "seleniumbase-website-scraper", "description": "SeleniumBase-style browser agent that turns observed website traffic into OpenAPI specs, coverage reports, samples, and a replay client.", - "version": "0.2.7", + "version": "0.2.8", "entrypoint": { "module": "agent", "class_name": "BrowserToApiAgent", @@ -381,7 +381,7 @@ "source": "python-a2a-pack", "project_manifest": { "name": "seleniumbase-website-scraper", - "version": "0.2.7", + "version": "0.2.8", "entrypoint": "agent:BrowserToApiAgent" } } diff --git a/a2a.yaml b/a2a.yaml index 37dec1b..b5576a5 100644 --- a/a2a.yaml +++ b/a2a.yaml @@ -1,5 +1,5 @@ name: seleniumbase-website-scraper -version: 0.2.7 +version: 0.2.8 entrypoint: agent:BrowserToApiAgent expose: public: true diff --git a/agent.py b/agent.py index e76eb91..c78c255 100644 --- a/agent.py +++ b/agent.py @@ -129,7 +129,7 @@ class BrowserToApiAgent(A2AAgent[BrowserToApiConfig, NoAuth]): "SeleniumBase-style browser agent that turns observed website traffic " "into OpenAPI specs, coverage reports, samples, and a replay client." ) - version = "0.2.7" + version = "0.2.8" config_model = BrowserToApiConfig auth_model = NoAuth @@ -383,6 +383,11 @@ def capture_browser_traffic( page.wait_for_timeout(wait_seconds * 1000) if ai_explore and ai_steps > 0 and llm_config: + log.append( + "ai: using ctx.llm " + f"model={llm_config.get('model', 'unknown')} " + f"source={llm_config.get('source', 'unknown')}" + ) _ai_explore_page( page, llm_config=llm_config, @@ -414,7 +419,13 @@ def _llm_config_from_ctx(ctx: RunContext[NoAuth]) -> dict[str, Any] | None: model = str(getattr(creds, "model", "") or "") if not (api_key and base_url and model): return None - return {"api_key": api_key, "base_url": base_url, "model": model} + return { + "api_key": api_key, + "base_url": base_url, + "model": model, + "source": str(getattr(creds, "source", "") or ""), + "extra_body": dict(getattr(creds, "extra_body", None) or {}), + } def _default_ai_goal(url: str) -> str: @@ -558,15 +569,48 @@ def _llm_choose_action( def _chat_completion_json(llm_config: dict[str, Any], *, system: str, user: str) -> str: url = f"{llm_config['base_url'].rstrip('/')}/chat/completions" - payload = { + base_payload = { "model": llm_config["model"], "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], - "max_tokens": 320, - "response_format": {"type": "json_object"}, } + extra_body = dict(llm_config.get("extra_body") or {}) + attempts = [ + { + **base_payload, + "max_completion_tokens": 1200, + "reasoning_effort": "minimal", + **extra_body, + }, + { + **base_payload, + "max_tokens": 800, + **extra_body, + }, + { + **base_payload, + "max_completion_tokens": 1200, + "response_format": {"type": "json_object"}, + **extra_body, + }, + ] + last_error: Exception | None = None + for payload in attempts: + try: + content = _post_chat_completion(url, llm_config["api_key"], payload) + except Exception as exc: # noqa: BLE001 + last_error = exc + continue + if content.strip(): + return content + if last_error is not None: + raise last_error + return "" + + +def _post_chat_completion(url: str, api_key: str, payload: dict[str, Any]) -> str: req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), @@ -586,7 +630,15 @@ def _chat_completion_json(llm_config: dict[str, Any], *, system: str, user: str) if not choices: return "" message = choices[0].get("message") if isinstance(choices[0], dict) else {} - return str(message.get("content") or "") + content = message.get("content") + if isinstance(content, list): + parts = [ + str(item.get("text") or item.get("content") or "") + for item in content + if isinstance(item, dict) + ] + return "\n".join(part for part in parts if part) + return str(content or "") def _apply_ai_action(