Skip to main content
Glama
analyzer.py8.02 kB
"""Skill analyzer for extracting skills from session recordings. Uses an LLM to identify the "money request" (the API call that returns the desired data) from recorded network traffic. """ import json import logging from typing import TYPE_CHECKING from .models import AuthRecovery, FallbackConfig, MoneyRequest, NavigationStep, SessionRecording, Skill, SkillHints, SkillParameter, SkillRequest from .prompts import ANALYSIS_SYSTEM_PROMPT, get_analysis_prompt if TYPE_CHECKING: from browser_use.llm.base import BaseChatModel logger = logging.getLogger(__name__) class SkillAnalyzer: """Analyzes session recordings to extract reusable skills. The analyzer uses an LLM to identify which API call (the "money request") returned the data the user asked for, and extracts parameters that can be templated for future executions. """ def __init__(self, llm: "BaseChatModel"): """Initialize analyzer. Args: llm: LLM instance to use for analysis """ self.llm = llm async def analyze(self, recording: SessionRecording) -> Skill | None: """Analyze a recording to extract a skill. Args: recording: Session recording with network events Returns: Extracted Skill if successful, None if no API found """ # Get API calls summary api_calls = recording.get_api_calls() if not api_calls: logger.warning("No API calls found in recording") return None # Format API calls for analysis api_calls_data = [] for req, resp in api_calls: call_data = { "url": req.url, "method": req.method, "status": resp.status, "content_type": resp.mime_type, "has_body": resp.body is not None, "post_data": req.post_data[:500] if req.post_data else None, "response_body": resp.body[:2000] if resp.body else None, } api_calls_data.append(call_data) # Build prompt prompt = get_analysis_prompt(recording.task, recording.result, api_calls_data) # Call LLM try: from browser_use.llm.messages import SystemMessage, UserMessage response = await self.llm.ainvoke([SystemMessage(content=ANALYSIS_SYSTEM_PROMPT), UserMessage(content=prompt)]) # Parse response - browser-use returns ChatInvokeCompletion with .completion result = self._parse_analysis_response(response.completion) if not result or not result.get("success"): reason = result.get("reason", "Unknown") if result else "Failed to parse response" logger.info(f"Skill analysis failed: {reason}") return None # Build skill from analysis skill = self._build_skill(result, recording) return skill except Exception as e: logger.error(f"Error during skill analysis: {e}") return None def _parse_analysis_response(self, content: str) -> dict | None: """Parse the LLM's analysis response. Args: content: Raw LLM response content Returns: Parsed JSON dict or None """ try: # Try to extract JSON from the response content = str(content).strip() # Handle markdown code blocks if "```json" in content: start = content.find("```json") + 7 end = content.find("```", start) content = content[start:end].strip() elif "```" in content: start = content.find("```") + 3 end = content.find("```", start) content = content[start:end].strip() return json.loads(content) except json.JSONDecodeError as e: logger.warning(f"Failed to parse analysis response: {e}") return None def _build_skill(self, analysis: dict, recording: SessionRecording) -> Skill: """Build a Skill object from analysis results. Args: analysis: Parsed analysis response recording: Original recording Returns: Skill object with direct execution support if possible """ # NEW: Build SkillRequest for direct execution request_data = analysis.get("request", {}) skill_request = None if request_data.get("url"): skill_request = SkillRequest( url=request_data.get("url", ""), method=request_data.get("method", "GET"), headers=request_data.get("headers", {}), body_template=request_data.get("body_template"), response_type=request_data.get("response_type", "json"), extract_path=request_data.get("extract_path"), html_selectors=request_data.get("html_selectors"), ) logger.info(f"Built SkillRequest for direct execution: {skill_request.url}") # NEW: Build AuthRecovery if provided auth_data = analysis.get("auth_recovery", {}) auth_recovery = None if auth_data.get("recovery_page"): 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"), ) # Build parameters from top-level or nested in request parameters_data = analysis.get("parameters", []) parameters = [ SkillParameter( name=p.get("name", ""), source=p.get("source", "query"), required=p.get("required", False), default=p.get("default"), ) for p in parameters_data ] # LEGACY: Build money_request for backward compatibility money_request_data = analysis.get("money_request", {}) money_request = None if money_request_data.get("endpoint"): money_request = MoneyRequest( endpoint=money_request_data.get("endpoint", ""), method=money_request_data.get("method", "GET"), content_type=money_request_data.get("content_type", "application/json"), response_path=money_request_data.get("response_path"), identifies_by=money_request_data.get("identifies_by"), ) # LEGACY: Build navigation steps navigation_data = analysis.get("navigation_steps", []) navigation = [NavigationStep(url_pattern=n.get("url_pattern", ""), description=n.get("description", "")) for n in navigation_data] # Build hints (legacy) hints = SkillHints(navigation=navigation, money_request=money_request) # Generate skill name if not provided skill_name = analysis.get("skill_name_suggestion", "") if not skill_name: # Generate from task skill_name = recording.task[:30].lower().replace(" ", "-").replace("'", "").replace('"', "") return Skill( name=skill_name, description=analysis.get("skill_description", recording.task), original_task=recording.task, request=skill_request, # NEW: Direct execution auth_recovery=auth_recovery, # NEW: Auth recovery hints=hints, parameters=parameters, fallback=FallbackConfig(), ) class SkillAnalysisResult: """Result of skill analysis.""" def __init__( self, success: bool, skill: Skill | None = None, reason: str = "", ): self.success = success self.skill = skill self.reason = reason def __bool__(self) -> bool: return self.success

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