Skip to main content
Glama
models.py18.2 kB
"""Data models for browser skills. Skills are MACHINE-GENERATED from learning sessions, not manually authored. The agent discovers API endpoints during learning mode, and the analyzer extracts the "money request" (the API call that returns the desired data). Execution uses browser's fetch() via CDP for: - Automatic cookie/session handling - No CORS issues (request from page context) - Preserved auth state """ from dataclasses import dataclass, field from datetime import datetime from typing import Any, Literal # Sensitive headers that should be stripped before saving skills SENSITIVE_HEADERS = frozenset( { "authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token", "x-csrf-token", "x-session-id", "bearer", "api-key", } ) def strip_sensitive_headers(headers: dict[str, str]) -> dict[str, str]: """Remove sensitive headers before saving skill. Unlike redaction, this completely removes sensitive headers rather than replacing values with '***REDACTED***'. """ return {k: v for k, v in headers.items() if k.lower() not in SENSITIVE_HEADERS} # --- Recording Models (captured during learning) --- @dataclass class NetworkRequest: """A captured network request during recording.""" url: str method: str headers: dict[str, str] = field(default_factory=dict) post_data: str | None = None resource_type: str = "" # XHR, Fetch, Document, etc. timestamp: float = 0.0 request_id: str = "" @dataclass class NetworkResponse: """A captured network response during recording.""" url: str status: int headers: dict[str, str] = field(default_factory=dict) body: str | None = None # Response body (if captured) mime_type: str = "" timestamp: float = 0.0 request_id: str = "" @dataclass class SessionRecording: """Complete recording of a browser session for skill extraction.""" task: str result: str requests: list[NetworkRequest] = field(default_factory=list) responses: list[NetworkResponse] = field(default_factory=list) navigation_urls: list[str] = field(default_factory=list) start_time: datetime = field(default_factory=datetime.now) end_time: datetime | None = None def get_api_calls(self) -> list[tuple[NetworkRequest, NetworkResponse]]: """Get paired request/response for XHR/Fetch calls only.""" api_requests = {r.request_id: r for r in self.requests if r.resource_type in ("XHR", "Fetch", "xhr", "fetch")} pairs = [] for resp in self.responses: if resp.request_id in api_requests: pairs.append((api_requests[resp.request_id], resp)) return pairs # --- Skill Models (machine-generated from analysis) --- @dataclass class MoneyRequest: """The key API call that returns the desired data. This is identified by the analyzer as THE request that contains the data the user asked for. DEPRECATED: Use SkillRequest for new skills. Kept for backward compatibility. """ endpoint: str # URL path (without domain) method: str = "GET" content_type: str = "application/json" request_template: str | None = None # Template for request body (with {param} placeholders) response_path: str | None = None # JSONPath to the data in response (e.g., "data.jobs") identifies_by: str | None = None # How to identify this request (e.g., "operationName: searchJobs") sample_response_schema: dict | None = None # Simplified schema of expected response # --- Direct Execution Models (new architecture) --- @dataclass class SkillRequest: """Complete request specification for direct browser execution. Contains everything needed to execute fetch() from within the browser: - Full URL with parameter placeholders - Method, headers, body template - Response parsing configuration """ # Request details url: str # Full URL with {param} placeholders, e.g., "https://npmjs.com/search?q={query}" method: str = "GET" headers: dict[str, str] = field(default_factory=dict) # Headers to send (non-sensitive) body_template: str | None = None # Request body template with {param} placeholders # Response handling response_type: Literal["json", "html", "text"] = "json" extract_path: str | None = None # For JSON: JMESPath like "data.items" or "objects[*].package" # For HTML responses - CSS selectors html_selectors: dict[str, str] | None = None # {"items": ".result-item", "title": "h3 a", ...} # Security: Domain allowlist (empty = allow all for backwards compatibility) allowed_domains: list[str] = field(default_factory=list) def build_url(self, params: dict[str, Any]) -> str: """Build URL by substituting parameter placeholders.""" url = self.url for key, value in params.items(): url = url.replace(f"{{{key}}}", str(value)) return url def build_body(self, params: dict[str, Any]) -> str | None: """Build request body by substituting parameter placeholders.""" if not self.body_template: return None body = self.body_template for key, value in params.items(): body = body.replace(f"{{{key}}}", str(value)) return body def get_safe_headers(self) -> dict[str, str]: """Return headers with sensitive ones removed (not redacted). Use this when saving skills to avoid storing auth tokens. """ return strip_sensitive_headers(self.headers) def to_fetch_options(self, params: dict[str, Any]) -> dict[str, Any]: """Generate JavaScript fetch() options.""" options: dict[str, Any] = { "method": self.method, "credentials": "include", # Always include cookies } if self.headers: options["headers"] = self.headers body = self.build_body(params) if body: options["body"] = body return options @dataclass class AuthRecovery: """Configuration for handling authentication failures. When a skill request returns 401/403, the runner can: 1. Navigate to the recovery page 2. Let the agent re-authenticate 3. Retry the original request """ # When to trigger recovery trigger_on_status: list[int] = field(default_factory=lambda: [401, 403]) trigger_on_body: str | None = None # Text in response body that indicates auth failure # Recovery action recovery_page: str = "" # URL to navigate to for re-auth (e.g., login page) success_indicator: str | None = None # How to know auth succeeded (e.g., "cookie:session present") # Limits max_retries: int = 1 @dataclass class NavigationStep: """A navigation step required before calling the API.""" url_pattern: str description: str required: bool = True @dataclass class SkillParameter: """A configurable parameter extracted from the API call.""" name: str type: str = "string" # string, integer, boolean required: bool = False default: str | None = None description: str = "" source: str = "" # Where this param was found: "url", "body", "query" @dataclass class SkillHints: """Hints for the agent to execute the skill efficiently.""" navigation: list[NavigationStep] = field(default_factory=list) money_request: MoneyRequest | None = None def to_prompt(self, params: dict) -> str: """Convert hints to a prompt string for the agent.""" lines = [] if self.navigation: lines.append("NAVIGATION STEPS:") for step in self.navigation: url = step.url_pattern for key, val in params.items(): url = url.replace(f"{{{key}}}", str(val)) lines.append(f" 1. {step.description}: {url}") lines.append("") if self.money_request: lines.append("TARGET API CALL:") lines.append(f" - Endpoint: {self.money_request.endpoint}") lines.append(f" - Method: {self.money_request.method}") if self.money_request.identifies_by: lines.append(f" - Identify by: {self.money_request.identifies_by}") if self.money_request.response_path: lines.append(f" - Data location: {self.money_request.response_path}") lines.append("") return "\n".join(lines) @dataclass class FallbackConfig: """Configuration for fallback behavior when hints fail.""" strategy: str = "explore_full" # explore_full, error, retry_with_delay max_retries: int = 2 @dataclass class Skill: """A machine-generated browser skill with API hints. Skills are created automatically when: 1. User runs run_browser_agent with learn=True 2. Agent successfully completes the task by discovering an API 3. Analyzer identifies the "money request" and extracts parameters Two execution modes: - Direct execution (new): Use `request` field, execute fetch() via CDP - Hint-based (legacy): Use `hints` field, agent navigates with guidance """ name: str description: str original_task: str # The task that created this skill # NEW: Direct execution configuration request: SkillRequest | None = None # If set, use direct fetch() execution auth_recovery: AuthRecovery | None = None # How to handle auth failures # LEGACY: Hint-based execution (agent navigates with guidance) hints: SkillHints = field(default_factory=SkillHints) parameters: list[SkillParameter] = field(default_factory=list) # Metadata version: int = 1 created: datetime = field(default_factory=datetime.now) last_used: datetime | None = None success_count: int = 0 failure_count: int = 0 fallback: FallbackConfig = field(default_factory=FallbackConfig) # Skill verification status status: Literal["draft", "verified", "failed"] = "draft" @property def supports_direct_execution(self) -> bool: """Check if this skill supports fast direct execution.""" return self.request is not None @property def success_rate(self) -> float: """Calculate success rate from usage statistics.""" total = self.success_count + self.failure_count return self.success_count / total if total > 0 else 0.0 def merge_params(self, user_params: dict[str, Any]) -> dict[str, Any]: """Merge user-provided params with parameter defaults. User params take precedence over defaults. """ merged = {} for param in self.parameters: if param.name in user_params: merged[param.name] = user_params[param.name] elif param.default is not None: merged[param.name] = param.default # Also include any extra user params not in schema for key, value in user_params.items(): if key not in merged: merged[key] = value return merged def to_dict(self) -> dict[str, Any]: """Convert skill to dictionary for serialization.""" result: dict[str, Any] = { "name": self.name, "description": self.description, "original_task": self.original_task, "version": self.version, "created": self.created.isoformat() if self.created else None, "last_used": self.last_used.isoformat() if self.last_used else None, "success_count": self.success_count, "failure_count": self.failure_count, "status": self.status, "parameters": [ { "name": p.name, "type": p.type, "required": p.required, "default": p.default, "description": p.description, "source": p.source, } for p in self.parameters ], "fallback": { "strategy": self.fallback.strategy, "max_retries": self.fallback.max_retries, }, } # Add request for direct execution (headers stripped, not redacted) if self.request: result["request"] = { "url": self.request.url, "method": self.request.method, "headers": self.request.get_safe_headers(), "body_template": self.request.body_template, "response_type": self.request.response_type, "extract_path": self.request.extract_path, "html_selectors": self.request.html_selectors, "allowed_domains": self.request.allowed_domains, } # NEW: Add auth_recovery if self.auth_recovery: result["auth_recovery"] = { "trigger_on_status": self.auth_recovery.trigger_on_status, "trigger_on_body": self.auth_recovery.trigger_on_body, "recovery_page": self.auth_recovery.recovery_page, "success_indicator": self.auth_recovery.success_indicator, "max_retries": self.auth_recovery.max_retries, } # LEGACY: Add hints for backward compatibility hints_dict: dict[str, Any] = { "navigation": [{"url_pattern": n.url_pattern, "description": n.description, "required": n.required} for n in self.hints.navigation], } # Add money_request if present (legacy) if self.hints.money_request: mr = self.hints.money_request hints_dict["money_request"] = { "endpoint": mr.endpoint, "method": mr.method, "content_type": mr.content_type, "request_template": mr.request_template, "response_path": mr.response_path, "identifies_by": mr.identifies_by, "sample_response_schema": mr.sample_response_schema, } result["hints"] = hints_dict return result @classmethod def from_dict(cls, data: dict[str, Any]) -> "Skill": """Create skill from dictionary.""" # Parse parameters parameters = [ SkillParameter( name=p["name"], type=p.get("type", "string"), required=p.get("required", False), default=p.get("default"), description=p.get("description", ""), source=p.get("source", ""), ) for p in data.get("parameters", []) ] # Parse request for direct execution request = None req_data = data.get("request") if req_data: request = SkillRequest( url=req_data["url"], method=req_data.get("method", "GET"), headers=req_data.get("headers", {}), body_template=req_data.get("body_template"), response_type=req_data.get("response_type", "json"), extract_path=req_data.get("extract_path"), html_selectors=req_data.get("html_selectors"), allowed_domains=req_data.get("allowed_domains", []), ) # NEW: Parse auth_recovery auth_recovery = None auth_data = data.get("auth_recovery") if auth_data: auth_recovery = AuthRecovery( trigger_on_status=auth_data.get("trigger_on_status", [401, 403]), trigger_on_body=auth_data.get("trigger_on_body"), recovery_page=auth_data.get("recovery_page", ""), success_indicator=auth_data.get("success_indicator"), max_retries=auth_data.get("max_retries", 1), ) # LEGACY: Parse hints hints_data = data.get("hints", {}) # Parse navigation steps navigation = [ NavigationStep( url_pattern=n["url_pattern"], description=n.get("description", ""), required=n.get("required", True), ) for n in hints_data.get("navigation", []) ] # Parse money_request (legacy) money_request = None mr_data = hints_data.get("money_request") if mr_data: money_request = MoneyRequest( endpoint=mr_data["endpoint"], method=mr_data.get("method", "GET"), content_type=mr_data.get("content_type", "application/json"), request_template=mr_data.get("request_template"), response_path=mr_data.get("response_path"), identifies_by=mr_data.get("identifies_by"), sample_response_schema=mr_data.get("sample_response_schema"), ) hints = SkillHints(navigation=navigation, money_request=money_request) # Parse fallback fallback_data = data.get("fallback", {}) fallback = FallbackConfig( strategy=fallback_data.get("strategy", "explore_full"), max_retries=fallback_data.get("max_retries", 2), ) # Parse dates created = data.get("created") if isinstance(created, str): created = datetime.fromisoformat(created) elif created is None: created = datetime.now() last_used = data.get("last_used") if isinstance(last_used, str): last_used = datetime.fromisoformat(last_used) return cls( name=data["name"], description=data.get("description", ""), original_task=data.get("original_task", ""), version=data.get("version", 1), created=created, last_used=last_used, success_count=data.get("success_count", 0), failure_count=data.get("failure_count", 0), status=data.get("status", "draft"), parameters=parameters, request=request, auth_recovery=auth_recovery, hints=hints, fallback=fallback, )

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Saik0s/mcp-browser-use'

If you have feedback or need assistance with the MCP directory API, please join our Discord server