From ffc45ec72616e4fe35019d0de29ce103b4be400d Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Tue, 21 Oct 2025 21:58:47 -0500 Subject: [PATCH 1/4] Cleanup eval-server and renamed to agent-server --- MODEL-CONFIGS.md | 8 +- {eval-server => agent-server}/.env.example | 0 agent-server/.gitignore | 10 + agent-server/README.md | 415 ++++++ .../nodejs/.env.example | 0 agent-server/nodejs/CLAUDE.md | 343 +++++ .../nodejs/README.md | 53 + agent-server/nodejs/clients/README.md | 41 + .../nodejs/clients/example-client.yaml | 15 + agent-server/nodejs/logs/requests.jsonl | 11 + agent-server/nodejs/package-lock.json | 401 +++++ agent-server/nodejs/package.json | 37 + agent-server/nodejs/src/api-server.js | 669 +++++++++ .../nodejs/src/client-manager.js | 189 +-- .../nodejs/src/config.js | 9 +- .../nodejs/src/lib/BrowserAgentServer.js | 603 ++++++-- .../nodejs/src/lib/HTTPWrapper.js | 32 +- agent-server/nodejs/src/lib/RequestStack.js | 85 ++ .../nodejs/src/lib/judges/Judge.js | 0 .../nodejs/src/lib/judges/LLMJudge.js | 0 .../nodejs/src/logger.js | 63 +- .../nodejs/src/rpc-client.js | 0 agent-server/nodejs/start.js | 39 + .../start.js | 32 +- eval-server/.gitignore | 3 - eval-server/README.md | 239 --- eval-server/nodejs/CLAUDE.md | 190 --- .../1233ae25-9f9e-4f77-924d-865f7d615cef.yaml | 12 - eval-server/nodejs/docs/CLIENT_SETUP.md | 445 ------ eval-server/nodejs/docs/PROTOCOL.md | 310 ---- .../nodejs/docs/TRIGGERING_EVALUATIONS.md | 306 ---- eval-server/nodejs/docs/YAML_SCHEMA.md | 315 ---- .../nodejs/evals/action-agent/a11y-001.yaml | 46 - .../evals/action-agent/accordion-001.yaml | 46 - .../action-agent/action-agent-a11y-001.yaml | 46 - .../action-agent-accordion-001.yaml | 46 - .../action-agent-autocomplete-001.yaml | 46 - .../action-agent-checkbox-001.yaml | 46 - .../action-agent-checkbox-002.yaml | 47 - .../action-agent/action-agent-click-001.yaml | 47 - .../action-agent-context-001.yaml | 46 - .../action-agent-datepicker-001.yaml | 46 - .../action-agent-daterange-001.yaml | 46 - .../action-agent-dropdown-001.yaml | 46 - .../action-agent-dynamic-001.yaml | 46 - .../action-agent-ecommerce-001.yaml | 46 - .../action-agent/action-agent-error-001.yaml | 47 - .../action-agent/action-agent-filter-001.yaml | 46 - .../action-agent/action-agent-form-001.yaml | 46 - .../action-agent/action-agent-hover-001.yaml | 46 - .../action-agent-keyboard-001.yaml | 46 - .../action-agent/action-agent-login-001.yaml | 47 - .../action-agent/action-agent-modal-001.yaml | 46 - .../action-agent-multiselect-001.yaml | 46 - .../action-agent-multistep-001.yaml | 47 - .../action-agent/action-agent-nav-001.yaml | 46 - .../action-agent/action-agent-radio-001.yaml | 47 - .../action-agent/action-agent-slider-001.yaml | 46 - .../action-agent-tableselect-001.yaml | 46 - .../action-agent-tablesort-001.yaml | 46 - .../action-agent/action-agent-tabs-001.yaml | 46 - .../action-agent-timepicker-001.yaml | 46 - .../action-agent/action-agent-upload-001.yaml | 46 - .../action-agent/action-agent-video-001.yaml | 47 - .../action-agent/action-agent-video-002.yaml | 47 - .../evals/action-agent/autocomplete-001.yaml | 46 - .../evals/action-agent/checkbox-001.yaml | 46 - .../evals/action-agent/checkbox-002.yaml | 47 - .../nodejs/evals/action-agent/click-001.yaml | 47 - .../evals/action-agent/context-001.yaml | 46 - .../evals/action-agent/datepicker-001.yaml | 46 - .../evals/action-agent/daterange-001.yaml | 46 - .../evals/action-agent/dropdown-001.yaml | 46 - .../evals/action-agent/dynamic-001.yaml | 46 - .../evals/action-agent/ecommerce-001.yaml | 46 - .../nodejs/evals/action-agent/error-001.yaml | 47 - .../nodejs/evals/action-agent/filter-001.yaml | 46 - .../nodejs/evals/action-agent/form-001.yaml | 46 - .../nodejs/evals/action-agent/hover-001.yaml | 46 - .../evals/action-agent/keyboard-001.yaml | 46 - .../nodejs/evals/action-agent/login-001.yaml | 47 - .../nodejs/evals/action-agent/modal-001.yaml | 46 - .../evals/action-agent/multiselect-001.yaml | 46 - .../evals/action-agent/multistep-001.yaml | 47 - .../nodejs/evals/action-agent/nav-001.yaml | 46 - .../nodejs/evals/action-agent/radio-001.yaml | 47 - .../nodejs/evals/action-agent/slider-001.yaml | 46 - .../evals/action-agent/tableselect-001.yaml | 46 - .../evals/action-agent/tablesort-001.yaml | 46 - .../nodejs/evals/action-agent/tabs-001.yaml | 46 - .../evals/action-agent/timepicker-001.yaml | 46 - .../nodejs/evals/action-agent/upload-001.yaml | 46 - .../nodejs/evals/action-agent/video-001.yaml | 47 - .../nodejs/evals/action-agent/video-002.yaml | 47 - eval-server/nodejs/evals/config.yaml | 11 - .../end-to-end/b-vitamins-research-001.yaml | 35 - .../end-to-end/investment-research-001.yaml | 35 - .../end-to-end/product-comparison-001.yaml | 40 - .../end-to-end/recipe-nutrition-001.yaml | 40 - .../evals/end-to-end/travel-planning-001.yaml | 40 - .../evals/research-agent/basic-001.yaml | 39 - .../evals/research-agent/business-001.yaml | 39 - .../evals/research-agent/comparison-001.yaml | 39 - .../evals/research-agent/current-001.yaml | 40 - .../nodejs/evals/research-agent/edge-001.yaml | 39 - .../research-agent-basic-001.yaml | 39 - .../research-agent-business-001.yaml | 39 - .../research-agent-comparison-001.yaml | 39 - .../research-agent-current-001.yaml | 40 - .../research-agent-edge-001.yaml | 39 - .../research-agent-technical-001.yaml | 39 - .../research-agent-tools-001.yaml | 40 - .../evals/research-agent/technical-001.yaml | 39 - .../evals/research-agent/tools-001.yaml | 40 - .../schema-extractor/amazon-product-001.yaml | 78 - .../evals/schema-extractor/bbc-news-001.yaml | 69 - .../schema-extractor/bing-search-001.yaml | 70 - .../github-repo-001-streamlined.yaml | 66 - .../schema-extractor/github-repo-001.yaml | 66 - .../schema-extractor/google-flights-001.yaml | 106 -- .../schema-extractor/google-search-001.yaml | 76 - .../evals/schema-extractor/homedepot-001.yaml | 92 -- .../evals/schema-extractor/macys-001.yaml | 106 -- .../wikipedia-search-001.yaml | 77 - .../dynamic-content-verification-001.yaml | 45 - .../screenshot-error-handling-001.yaml | 42 - .../screenshot-fullpage-001.yaml | 43 - .../screenshot-viewport-001.yaml | 42 - .../visual-comparison-001.yaml | 45 - .../amazon-product-001.yaml | 78 - .../bbc-news-001.yaml | 69 - .../bing-search-001.yaml | 70 - .../github-repo-001.yaml | 66 - .../google-flights-001.yaml | 106 -- .../google-search-001.yaml | 76 - .../homedepot-001.yaml | 92 -- .../macys-001.yaml | 106 -- .../wikipedia-001.yaml | 76 - .../wikipedia-search-001.yaml | 77 - .../evals/web-task-agent/booking-001.yaml | 45 - .../evals/web-task-agent/ecommerce-001.yaml | 53 - .../evals/web-task-agent/error-001.yaml | 45 - .../evals/web-task-agent/extract-001.yaml | 60 - .../evals/web-task-agent/finance-001.yaml | 68 - .../evals/web-task-agent/flight-001.yaml | 45 - .../nodejs/evals/web-task-agent/food-001.yaml | 68 - .../evals/web-task-agent/iframe-001.yaml | 83 -- .../nodejs/evals/web-task-agent/jobs-001.yaml | 68 - .../evals/web-task-agent/learning-001.yaml | 69 - .../nodejs/evals/web-task-agent/nav-001.yaml | 46 - .../nodejs/evals/web-task-agent/news-001.yaml | 64 - .../evals/web-task-agent/realestate-001.yaml | 70 - .../evals/web-task-agent/scroll-001.yaml | 61 - .../evals/web-task-agent/scroll-002.yaml | 65 - .../evals/web-task-agent/scroll-003.yaml | 61 - .../evals/web-task-agent/scroll-004.yaml | 61 - .../evals/web-task-agent/scroll-005.yaml | 73 - .../evals/web-task-agent/search-001.yaml | 41 - .../evals/web-task-agent/social-001.yaml | 60 - .../web-task-agent-booking-001.yaml | 45 - .../web-task-agent-ecommerce-001.yaml | 53 - .../web-task-agent-error-001.yaml | 45 - .../web-task-agent-extract-001.yaml | 60 - .../web-task-agent-finance-001.yaml | 68 - .../web-task-agent-flight-001.yaml | 45 - .../web-task-agent-food-001.yaml | 68 - .../web-task-agent-iframe-001.yaml | 83 -- .../web-task-agent-jobs-001.yaml | 68 - .../web-task-agent-learning-001.yaml | 69 - .../web-task-agent-nav-001.yaml | 46 - .../web-task-agent-news-001.yaml | 64 - .../web-task-agent-realestate-001.yaml | 70 - .../web-task-agent-scroll-001.yaml | 61 - .../web-task-agent-scroll-002.yaml | 65 - .../web-task-agent-scroll-003.yaml | 61 - .../web-task-agent-scroll-004.yaml | 61 - .../web-task-agent-scroll-005.yaml | 73 - .../web-task-agent-search-001.yaml | 41 - .../web-task-agent-social-001.yaml | 60 - .../1233ae25-9f9e-4f77-924d-865f7d615cef.yaml | 12 - eval-server/nodejs/examples/library-usage.js | 250 ---- eval-server/nodejs/examples/logs/.gitignore | 3 - eval-server/nodejs/examples/multiple-evals.js | 167 --- eval-server/nodejs/logs/.gitignore | 2 - eval-server/nodejs/package-lock.json | 832 ----------- eval-server/nodejs/package.json | 43 - eval-server/nodejs/schemas/client.schema.json | 299 ---- eval-server/nodejs/src/api-server.js | 492 ------- eval-server/nodejs/src/cli/CLI.js | 518 ------- eval-server/nodejs/src/cli/index.js | 23 - eval-server/nodejs/src/evaluator.js | 117 -- .../nodejs/src/lib/EvaluationLoader.js | 448 ------ eval-server/nodejs/src/lib/EvaluationStack.js | 85 -- .../nodejs/templates/default-client.yaml | 56 - eval-server/python/README.md | 368 ----- eval-server/python/UV_COMMANDS.md | 188 --- eval-server/python/evals/README.md | 195 --- .../python/evals/browsecomp_dataset.py | 252 ---- .../python/evals/browsecomp_eval_server.py | 836 ----------- eval-server/python/evals/browsecomp_scorer.py | 328 ----- .../evals/run_browsecomp_eval_server.sh | 12 - eval-server/python/examples/__init__.py | 10 - eval-server/python/examples/basic_server.py | 100 -- eval-server/python/examples/logs/.gitignore | 2 - .../python/examples/programmatic_evals.py | 428 ------ eval-server/python/examples/with_stack.py | 201 --- eval-server/python/logs/.gitignore | 2 - eval-server/python/pyproject.toml | 84 -- eval-server/python/quick_test.py | 38 - eval-server/python/requirements.txt | 10 - eval-server/python/run.py | 100 -- eval-server/python/scripts.py | 68 - .../python/src/bo_eval_server/__init__.py | 29 - .../src/bo_eval_server/client_manager.py | 401 ----- .../python/src/bo_eval_server/config.py | 75 - .../python/src/bo_eval_server/eval_server.py | 292 ---- .../src/bo_eval_server/evaluation_stack.py | 102 -- .../python/src/bo_eval_server/logger.py | 180 --- .../python/src/bo_eval_server/rpc_client.py | 229 --- eval-server/python/test_client.py | 190 --- eval-server/python/uv.lock | 1306 ----------------- 221 files changed, 2664 insertions(+), 19454 deletions(-) rename {eval-server => agent-server}/.env.example (100%) create mode 100644 agent-server/.gitignore create mode 100644 agent-server/README.md rename {eval-server => agent-server}/nodejs/.env.example (100%) create mode 100644 agent-server/nodejs/CLAUDE.md rename {eval-server => agent-server}/nodejs/README.md (90%) create mode 100644 agent-server/nodejs/clients/README.md create mode 100644 agent-server/nodejs/clients/example-client.yaml create mode 100644 agent-server/nodejs/logs/requests.jsonl create mode 100644 agent-server/nodejs/package-lock.json create mode 100644 agent-server/nodejs/package.json create mode 100644 agent-server/nodejs/src/api-server.js rename {eval-server => agent-server}/nodejs/src/client-manager.js (61%) rename {eval-server => agent-server}/nodejs/src/config.js (85%) rename eval-server/nodejs/src/lib/EvalServer.js => agent-server/nodejs/src/lib/BrowserAgentServer.js (59%) rename {eval-server => agent-server}/nodejs/src/lib/HTTPWrapper.js (66%) create mode 100644 agent-server/nodejs/src/lib/RequestStack.js rename {eval-server => agent-server}/nodejs/src/lib/judges/Judge.js (100%) rename {eval-server => agent-server}/nodejs/src/lib/judges/LLMJudge.js (100%) rename {eval-server => agent-server}/nodejs/src/logger.js (59%) rename {eval-server => agent-server}/nodejs/src/rpc-client.js (100%) create mode 100644 agent-server/nodejs/start.js rename eval-server/nodejs/examples/with-http-wrapper.js => agent-server/start.js (55%) delete mode 100644 eval-server/.gitignore delete mode 100644 eval-server/README.md delete mode 100644 eval-server/nodejs/CLAUDE.md delete mode 100644 eval-server/nodejs/clients/1233ae25-9f9e-4f77-924d-865f7d615cef.yaml delete mode 100644 eval-server/nodejs/docs/CLIENT_SETUP.md delete mode 100644 eval-server/nodejs/docs/PROTOCOL.md delete mode 100644 eval-server/nodejs/docs/TRIGGERING_EVALUATIONS.md delete mode 100644 eval-server/nodejs/docs/YAML_SCHEMA.md delete mode 100644 eval-server/nodejs/evals/action-agent/a11y-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/accordion-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-a11y-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-accordion-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-autocomplete-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-checkbox-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-checkbox-002.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-click-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-context-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-datepicker-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-daterange-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-dropdown-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-dynamic-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-ecommerce-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-error-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-filter-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-form-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-hover-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-keyboard-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-login-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-modal-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-multiselect-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-multistep-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-nav-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-radio-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-slider-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-tableselect-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-tablesort-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-tabs-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-timepicker-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-upload-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-video-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/action-agent-video-002.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/autocomplete-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/checkbox-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/checkbox-002.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/click-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/context-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/datepicker-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/daterange-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/dropdown-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/dynamic-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/ecommerce-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/error-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/filter-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/form-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/hover-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/keyboard-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/login-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/modal-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/multiselect-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/multistep-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/nav-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/radio-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/slider-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/tableselect-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/tablesort-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/tabs-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/timepicker-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/upload-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/video-001.yaml delete mode 100644 eval-server/nodejs/evals/action-agent/video-002.yaml delete mode 100644 eval-server/nodejs/evals/config.yaml delete mode 100644 eval-server/nodejs/evals/end-to-end/b-vitamins-research-001.yaml delete mode 100644 eval-server/nodejs/evals/end-to-end/investment-research-001.yaml delete mode 100644 eval-server/nodejs/evals/end-to-end/product-comparison-001.yaml delete mode 100644 eval-server/nodejs/evals/end-to-end/recipe-nutrition-001.yaml delete mode 100644 eval-server/nodejs/evals/end-to-end/travel-planning-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/basic-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/business-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/comparison-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/current-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/edge-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/research-agent-basic-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/research-agent-business-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/research-agent-comparison-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/research-agent-current-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/research-agent-edge-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/research-agent-technical-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/research-agent-tools-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/technical-001.yaml delete mode 100644 eval-server/nodejs/evals/research-agent/tools-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/amazon-product-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/bbc-news-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/bing-search-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/github-repo-001-streamlined.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/github-repo-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/google-flights-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/google-search-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/homedepot-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/macys-001.yaml delete mode 100644 eval-server/nodejs/evals/schema-extractor/wikipedia-search-001.yaml delete mode 100644 eval-server/nodejs/evals/screenshot-verification/dynamic-content-verification-001.yaml delete mode 100644 eval-server/nodejs/evals/screenshot-verification/screenshot-error-handling-001.yaml delete mode 100644 eval-server/nodejs/evals/screenshot-verification/screenshot-fullpage-001.yaml delete mode 100644 eval-server/nodejs/evals/screenshot-verification/screenshot-viewport-001.yaml delete mode 100644 eval-server/nodejs/evals/screenshot-verification/visual-comparison-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/amazon-product-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/bbc-news-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/bing-search-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/github-repo-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/google-flights-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/google-search-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/homedepot-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/macys-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/wikipedia-001.yaml delete mode 100644 eval-server/nodejs/evals/streamlined-schema-extractor/wikipedia-search-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/booking-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/ecommerce-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/error-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/extract-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/finance-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/flight-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/food-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/iframe-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/jobs-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/learning-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/nav-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/news-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/realestate-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/scroll-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/scroll-002.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/scroll-003.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/scroll-004.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/scroll-005.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/search-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/social-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-booking-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-ecommerce-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-error-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-extract-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-finance-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-flight-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-food-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-iframe-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-jobs-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-learning-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-nav-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-news-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-realestate-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-002.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-003.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-004.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-005.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-search-001.yaml delete mode 100644 eval-server/nodejs/evals/web-task-agent/web-task-agent-social-001.yaml delete mode 100644 eval-server/nodejs/examples/clients/1233ae25-9f9e-4f77-924d-865f7d615cef.yaml delete mode 100644 eval-server/nodejs/examples/library-usage.js delete mode 100644 eval-server/nodejs/examples/logs/.gitignore delete mode 100755 eval-server/nodejs/examples/multiple-evals.js delete mode 100644 eval-server/nodejs/logs/.gitignore delete mode 100644 eval-server/nodejs/package-lock.json delete mode 100644 eval-server/nodejs/package.json delete mode 100644 eval-server/nodejs/schemas/client.schema.json delete mode 100644 eval-server/nodejs/src/api-server.js delete mode 100644 eval-server/nodejs/src/cli/CLI.js delete mode 100644 eval-server/nodejs/src/cli/index.js delete mode 100644 eval-server/nodejs/src/evaluator.js delete mode 100644 eval-server/nodejs/src/lib/EvaluationLoader.js delete mode 100644 eval-server/nodejs/src/lib/EvaluationStack.js delete mode 100644 eval-server/nodejs/templates/default-client.yaml delete mode 100644 eval-server/python/README.md delete mode 100644 eval-server/python/UV_COMMANDS.md delete mode 100644 eval-server/python/evals/README.md delete mode 100644 eval-server/python/evals/browsecomp_dataset.py delete mode 100755 eval-server/python/evals/browsecomp_eval_server.py delete mode 100644 eval-server/python/evals/browsecomp_scorer.py delete mode 100755 eval-server/python/evals/run_browsecomp_eval_server.sh delete mode 100644 eval-server/python/examples/__init__.py delete mode 100644 eval-server/python/examples/basic_server.py delete mode 100644 eval-server/python/examples/logs/.gitignore delete mode 100644 eval-server/python/examples/programmatic_evals.py delete mode 100644 eval-server/python/examples/with_stack.py delete mode 100644 eval-server/python/logs/.gitignore delete mode 100644 eval-server/python/pyproject.toml delete mode 100644 eval-server/python/quick_test.py delete mode 100644 eval-server/python/requirements.txt delete mode 100644 eval-server/python/run.py delete mode 100644 eval-server/python/scripts.py delete mode 100644 eval-server/python/src/bo_eval_server/__init__.py delete mode 100644 eval-server/python/src/bo_eval_server/client_manager.py delete mode 100644 eval-server/python/src/bo_eval_server/config.py delete mode 100644 eval-server/python/src/bo_eval_server/eval_server.py delete mode 100644 eval-server/python/src/bo_eval_server/evaluation_stack.py delete mode 100644 eval-server/python/src/bo_eval_server/logger.py delete mode 100644 eval-server/python/src/bo_eval_server/rpc_client.py delete mode 100644 eval-server/python/test_client.py delete mode 100644 eval-server/python/uv.lock diff --git a/MODEL-CONFIGS.md b/MODEL-CONFIGS.md index 02eb8ebdaa..c80cc1f7b5 100644 --- a/MODEL-CONFIGS.md +++ b/MODEL-CONFIGS.md @@ -267,12 +267,12 @@ private async handleLLMConfigurationRequest(request: LLMConfigurationRequest): P ### Per-Request Override (Temporary) ```typescript -// eval-server sends evaluation with custom config +// agent-server sends request with custom config { "jsonrpc": "2.0", "method": "evaluate", "params": { - "evaluationId": "eval-123", + "requestId": "req-123", "tool": "chat", "input": { "message": "Hello" }, "model": { @@ -287,7 +287,7 @@ private async handleLLMConfigurationRequest(request: LLMConfigurationRequest): P ### Persistent Configuration ```typescript -// eval-server sets persistent configuration +// agent-server sets persistent configuration { "jsonrpc": "2.0", "method": "configure_llm", @@ -346,7 +346,7 @@ private async handleLLMConfigurationRequest(request: LLMConfigurationRequest): P ### ✅ Phase 3: Persistent Configuration API (COMPLETED) 8. ✅ **Extend EvaluationProtocol** with `configure_llm` method 9. ✅ **Implement `configure_llm` handler** in EvaluationAgent -10. 🔄 **Add eval-server support** for persistent configuration (server-side implementation needed) +10. 🔄 **Add agent-server support** for persistent configuration (server-side implementation needed) 11. ✅ **Add configuration validation** and error handling ### 🔄 Phase 4: Testing & Documentation (NEXT) diff --git a/eval-server/.env.example b/agent-server/.env.example similarity index 100% rename from eval-server/.env.example rename to agent-server/.env.example diff --git a/agent-server/.gitignore b/agent-server/.gitignore new file mode 100644 index 0000000000..a1e6addff8 --- /dev/null +++ b/agent-server/.gitignore @@ -0,0 +1,10 @@ +.env +node_modules +*.log + +# Exclude all client configuration files +nodejs/clients/*.yaml +nodejs/clients/*.yml + +# But keep the example file +!nodejs/clients/example-client.yaml diff --git a/agent-server/README.md b/agent-server/README.md new file mode 100644 index 0000000000..01af8cd79e --- /dev/null +++ b/agent-server/README.md @@ -0,0 +1,415 @@ +# Eval-Server + +HTTP API wrapper for Browser Operator - provides WebSocket server with Chrome DevTools Protocol (CDP) integration for browser automation. + +## Overview + +The eval-server exposes browser automation capabilities via HTTP API endpoints. It manages WebSocket connections to browser agents and provides REST APIs for: +- Sending tasks to agents (`/v1/responses`) +- Capturing screenshots via CDP +- Retrieving page content +- Managing browser tabs + +**Note:** Evaluation orchestration and LLM-as-a-judge logic lives in the separate `evals/` Python project, which calls these APIs. + +## Architecture + +``` +eval-server/ +└── nodejs/ + ├── src/ + │ ├── api-server.js # HTTP REST API endpoints + │ ├── client-manager.js # WebSocket client management + │ ├── rpc-client.js # JSON-RPC 2.0 communication + │ ├── config.js # Configuration management + │ ├── logger.js # Winston logging + │ └── lib/ + │ ├── EvalServer.js # Core server + CDP integration + │ └── HTTPWrapper.js # HTTP API wrapper + └── package.json +``` + +## Quick Start + +### Installation + +```bash +cd eval-server/nodejs +npm install +``` + +### Configuration + +Copy `.env.example` to `.env` and configure: + +```bash +# WebSocket server port +PORT=8080 + +# HTTP API server port +API_PORT=8081 + +# Authentication +AUTH_KEY=your-secret-key + +# Chrome DevTools Protocol endpoint +CDP_HOST=localhost +CDP_PORT=9223 +``` + +### Start Server + +```bash +npm start +``` + +The server will start: +- WebSocket server on `ws://localhost:8080` +- HTTP API server on `http://localhost:8081` + +## HTTP API Endpoints + +### Core Endpoint + +#### `POST /v1/responses` + +Send a task to a connected browser agent and get response. + +**Request:** +```json +{ + "input": "Click the submit button", + "url": "https://example.com", + "wait_timeout": 5000, + "model": { + "main_model": { + "provider": "openai", + "model": "gpt-5-mini", + "api_key": "sk-..." + }, + "mini_model": { + "provider": "openai", + "model": "gpt-5-nano", + "api_key": "sk-..." + }, + "nano_model": { + "provider": "openai", + "model": "gpt-5-nano", + "api_key": "sk-..." + } + } +} +``` + +**Response:** +```json +[ + { + "id": "msg_abc123", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Done - clicked submit button", + "annotations": [] + } + ], + "metadata": { + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "482D56EE57B1931A3B9D1BFDAF935429" + } + } +] +``` + +The `metadata` field contains `clientId` and `tabId` which can be used for screenshot capture and other CDP operations. + +### CDP Endpoints + +#### `POST /page/screenshot` + +Capture screenshot of a specific browser tab. + +**Request:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "482D56EE57B1931A3B9D1BFDAF935429", + "fullPage": false +} +``` + +**Response:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "482D56EE57B1931A3B9D1BFDAF935429", + "imageData": "data:image/png;base64,iVBORw0KG...", + "format": "png", + "fullPage": false, + "timestamp": 1234567890 +} +``` + +#### `POST /page/content` + +Get HTML or text content of a page. + +**Request:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "482D56EE57B1931A3B9D1BFDAF935429", + "format": "html" +} +``` + +**Response:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "482D56EE57B1931A3B9D1BFDAF935429", + "content": "...", + "format": "html", + "length": 12345, + "timestamp": 1234567890 +} +``` + +#### `POST /tabs/open` + +Open a new browser tab. + +**Request:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "url": "https://example.com", + "background": false +} +``` + +**Response:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "NEW_TAB_ID", + "compositeClientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306:NEW_TAB_ID", + "url": "https://example.com", + "status": "opened" +} +``` + +#### `POST /tabs/close` + +Close a browser tab. + +**Request:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "TAB_ID_TO_CLOSE" +} +``` + +**Response:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "TAB_ID_TO_CLOSE", + "status": "closed", + "success": true +} +``` + +### Status Endpoints + +#### `GET /status` + +Get server health and connected clients. + +**Response:** +```json +{ + "server": { + "running": true, + "uptime": 12345, + "connections": 1 + }, + "clients": [ + { + "id": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "name": "DevTools Client", + "connected": true, + "ready": true + } + ] +} +``` + +#### `GET /clients` + +List all connected clients with their tabs. + +**Response:** +```json +[ + { + "id": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "name": "DevTools Client", + "description": "Browser automation agent", + "tabCount": 3, + "tabs": [ + { + "tabId": "482D56EE57B1931A3B9D1BFDAF935429", + "compositeClientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306:482D56EE57B1931A3B9D1BFDAF935429", + "connected": true, + "ready": true, + "connectedAt": "2025-01-15T10:30:00.000Z", + "remoteAddress": "::ffff:172.18.0.1" + } + ] + } +] +``` + +## WebSocket Protocol + +Browser agents connect to the WebSocket server and implement JSON-RPC 2.0 protocol. + +### Connection Flow + +1. **Connect to WebSocket** + ``` + ws://localhost:8080 + ``` + +2. **Send Registration** + ```json + { + "type": "register", + "clientId": "unique-client-id", + "secretKey": "your-auth-key", + "capabilities": ["browser-automation"] + } + ``` + +3. **Send Ready Signal** + ```json + { + "type": "ready" + } + ``` + +4. **Handle RPC Calls** + + Server sends JSON-RPC 2.0 requests: + ```json + { + "jsonrpc": "2.0", + "method": "evaluate", + "params": { + "tool": "action_agent", + "input": {"objective": "Click submit button"}, + "model": {...} + }, + "id": "request-id" + } + ``` + + Agent responds: + ```json + { + "jsonrpc": "2.0", + "id": "request-id", + "result": { + "status": "completed", + "output": "Task completed successfully" + } + } + ``` + +## Chrome DevTools Protocol Setup + +The browser must be started with remote debugging enabled: + +```bash +chromium --remote-debugging-port=9223 +``` + +The CDP endpoint is accessible at: +- HTTP: `http://localhost:9223/json/version` +- WebSocket: `ws://localhost:9223/devtools/browser/{browserId}` + +## Usage with Evals Framework + +The eval-server is designed to work with the separate `evals/` Python project: + +1. **Start eval-server:** + ```bash + cd eval-server/nodejs + npm start + ``` + +2. **Run evaluations from evals/:** + ```bash + cd evals + python3 run.py --path action-agent/accordion-001.yaml --verbose + ``` + +The evals framework: +- Sends tasks to `/v1/responses` endpoint +- Extracts `clientId` and `tabId` from response metadata +- Captures screenshots via `/page/screenshot` +- Uses LLM judges (LLMJudge, VisionJudge) to evaluate results +- Generates reports and saves screenshots + +See `evals/README.md` for detailed evaluation framework documentation. + +## Dependencies + +Core dependencies: +- **ws** - WebSocket server +- **uuid** - ID generation +- **winston** - Structured logging +- **dotenv** - Environment variable management + +## Logging + +Logs are written to `logs/` directory (auto-created): +- `combined.log` - All log events +- `error.log` - Error events only +- `api.log` - API request/response logs + +## Docker Integration + +The eval-server runs inside the `kernel-browser-extended` Docker container. Volume mount for live development: + +```yaml +# docker-compose.yml +volumes: + - "./eval-server/nodejs:/opt/eval-server" +``` + +## Development + +```bash +# Install dependencies +npm install + +# Start server +npm start + +# Check status +curl http://localhost:8081/status + +# Test screenshot capture +curl -X POST http://localhost:8081/page/screenshot \ + -H "Content-Type: application/json" \ + -d '{"clientId":"CLIENT_ID","tabId":"TAB_ID","fullPage":false}' +``` + +## License + +MIT License diff --git a/eval-server/nodejs/.env.example b/agent-server/nodejs/.env.example similarity index 100% rename from eval-server/nodejs/.env.example rename to agent-server/nodejs/.env.example diff --git a/agent-server/nodejs/CLAUDE.md b/agent-server/nodejs/CLAUDE.md new file mode 100644 index 0000000000..cb4ade4eac --- /dev/null +++ b/agent-server/nodejs/CLAUDE.md @@ -0,0 +1,343 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +The eval-server is a **thin HTTP API wrapper for Browser Operator**. It provides: +- WebSocket server for browser agent connections +- REST APIs for browser automation via Chrome DevTools Protocol (CDP) +- JSON-RPC 2.0 bidirectional communication +- Screenshot capture and page content retrieval + +**Important:** Evaluation orchestration and LLM-as-a-judge logic lives in the separate `evals/` Python project, which calls these APIs. + +## Architecture + +### Separation of Concerns + +**eval-server (Node.js)**: Thin API layer +- WebSocket server for agent connections +- JSON-RPC 2.0 bidirectional communication +- REST APIs for CDP operations (screenshots, page content, tab management) +- NO evaluation logic, NO judges, NO test orchestration + +**evals (Python)**: Evaluation orchestration and judging +- LLM judges (LLMJudge, VisionJudge) in `lib/judge.py` +- Evaluation runners that call eval-server APIs +- Test case definitions (YAML files in `data/`) +- Result reporting and analysis + +## Core Components + +### WebSocket Server (src/lib/EvalServer.js) +- Accepts connections from browser agents (DevTools clients) +- Manages agent lifecycle (connect, ready, disconnect) +- Handles bidirectional RPC communication +- Integrates directly with Chrome DevTools Protocol + +### HTTP API Server (src/api-server.js) +- Exposes REST endpoints for external callers (e.g., Python evals) +- Main endpoint: `POST /v1/responses` - Send task to agent +- CDP endpoints: screenshot, page content, tab management +- Returns metadata (clientId, tabId) for subsequent operations + +### RPC Client (src/rpc-client.js) +- Implements JSON-RPC 2.0 protocol for bidirectional communication +- Manages request/response correlation with unique IDs +- Handles timeouts and error conditions +- Calls `evaluate(params)` method on connected agents + +### Client Manager (src/client-manager.js) +- Tracks WebSocket client connections +- Manages tab-level connections (composite clientId:tabId) +- Maintains client state (connected, ready) + +### CDP Integration (src/lib/EvalServer.js) +- Direct Chrome DevTools Protocol communication +- Screenshot capture via `Page.captureScreenshot` +- Page content access via `Runtime.evaluate` +- Tab management via `Target.createTarget` / `Target.closeTarget` + +### Logger (src/logger.js) +- Structured logging using Winston +- Separate log files for different event types +- JSON format for easy parsing and analysis + +## Key API Endpoints + +### POST /v1/responses + +Primary endpoint for sending tasks to browser agents. + +**Request:** +```json +{ + "input": "Click the submit button", + "url": "https://example.com", + "wait_timeout": 5000, + "model": { + "main_model": {"provider": "openai", "model": "gpt-5-mini", "api_key": "sk-..."}, + "mini_model": {"provider": "openai", "model": "gpt-5-nano", "api_key": "sk-..."}, + "nano_model": {"provider": "openai", "model": "gpt-5-nano", "api_key": "sk-..."} + } +} +``` + +**Response (OpenAI-compatible format):** +```json +[ + { + "id": "msg_abc123", + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "Done", "annotations": []}], + "metadata": { + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "482D56EE57B1931A3B9D1BFDAF935429" + } + } +] +``` + +**Important:** The `metadata` field contains `clientId` and `tabId` which are used by the evals framework for screenshot capture. + +### POST /page/screenshot + +Capture screenshot of a browser tab via CDP. + +**Request:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "482D56EE57B1931A3B9D1BFDAF935429", + "fullPage": false +} +``` + +**Response:** +```json +{ + "clientId": "...", + "tabId": "...", + "imageData": "data:image/png;base64,iVBORw0KG...", + "format": "png", + "timestamp": 1234567890 +} +``` + +### POST /page/content + +Get HTML or text content of a page. + +**Request:** +```json +{ + "clientId": "9907fd8d-92a8-4a6a-bce9-458ec8c57306", + "tabId": "482D56EE57B1931A3B9D1BFDAF935429", + "format": "html" +} +``` + +### POST /tabs/open, POST /tabs/close + +Tab management via CDP. + +## Configuration + +All configuration is managed through environment variables and `src/config.js`: + +```bash +# .env file +PORT=8080 # WebSocket server port +API_PORT=8081 # HTTP API server port +AUTH_KEY=hello # Authentication key +CDP_HOST=localhost # Chrome DevTools Protocol host +CDP_PORT=9223 # Chrome DevTools Protocol port +``` + +## Model Configuration Schema + +The server uses a canonical nested model configuration format: + +```typescript +interface ModelTierConfig { + provider: string; // "openai" | "groq" | "openrouter" | "litellm" + model: string; // Model name (e.g., "gpt-5-mini") + api_key: string; // API key for this tier +} + +interface ModelConfig { + main_model: ModelTierConfig; // Primary model + mini_model: ModelTierConfig; // Secondary model + nano_model: ModelTierConfig; // Tertiary model +} +``` + +## Tab Management + +Each browser tab is identified by a **composite client ID**: `baseClientId:tabId` + +Example: +- Base Client ID: `9907fd8d-92a8-4a6a-bce9-458ec8c57306` +- Tab ID: `482D56EE57B1931A3B9D1BFDAF935429` +- Composite: `9907fd8d-92a8-4a6a-bce9-458ec8c57306:482D56EE57B1931A3B9D1BFDAF935429` + +## CDP Communication Pattern + +The server uses **direct CDP communication**: + +1. Discover CDP WebSocket endpoint via `http://localhost:9223/json/version` +2. For each command, establish WebSocket connection to CDP endpoint +3. Send commands using JSON-RPC 2.0: + - **Browser-level**: `Target.createTarget`, `Target.closeTarget` + - **Tab-level**: `Runtime.evaluate`, `Page.captureScreenshot` +4. For tab-level operations: attach → execute → detach +5. Close WebSocket after receiving response + +## Integration with Evals Framework + +The eval-server is designed to work with the separate `evals/` Python project: + +**Flow:** +1. Python evals runner sends request to `/v1/responses` +2. Eval-server forwards to DevTools agent via WebSocket/JSON-RPC +3. Agent performs browser automation task +4. Response includes `metadata.clientId` and `metadata.tabId` +5. Python evals uses these IDs to capture screenshot via `/page/screenshot` +6. Python evals uses VisionJudge to evaluate with screenshot +7. Python evals generates report and saves screenshot + +See `evals/README.md` for detailed evaluation framework documentation. + +## Development Commands + +```bash +# Install dependencies +npm install + +# Start server +npm start + +# Check status +curl http://localhost:8081/status + +# Test screenshot +curl -X POST http://localhost:8081/page/screenshot \ + -H "Content-Type: application/json" \ + -d '{"clientId":"CLIENT_ID","tabId":"TAB_ID","fullPage":false}' +``` + +## Chrome Setup + +The browser must be started with remote debugging enabled: + +```bash +chromium --remote-debugging-port=9223 +``` + +CDP endpoint: `http://localhost:9223/json/version` + +## File Structure + +``` +nodejs/ +├── package.json +├── .env.example +└── src/ + ├── api-server.js # HTTP REST API endpoints + ├── client-manager.js # WebSocket client management + ├── rpc-client.js # JSON-RPC 2.0 communication + ├── config.js # Configuration management + ├── logger.js # Winston logging + └── lib/ + ├── EvalServer.js # Core server + CDP integration + └── HTTPWrapper.js # HTTP wrapper around EvalServer +``` + +## Key Implementation Details + +### formatResponse() Method + +Located in `src/api-server.js:706` + +Converts agent responses to OpenAI-compatible format and **adds metadata**: + +```javascript +formatResponse(responseText, clientId = null, tabId = null) { + const messageId = `msg_${uuidv4().replace(/-/g, '')}`; + + const response = [{ + id: messageId, + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: responseText, annotations: [] }] + }]; + + // Add metadata for screenshot capture + if (clientId && tabId) { + response[0].metadata = { clientId, tabId }; + } + + return response; +} +``` + +This metadata is critical for the evals framework to capture screenshots. + +### Screenshot Capture Flow + +1. Evals calls `/v1/responses` with task +2. Server returns response with `metadata: {clientId, tabId}` +3. Evals extracts metadata from response +4. Evals calls `/page/screenshot` with extracted IDs +5. Server uses CDP to capture screenshot +6. Returns base64-encoded PNG +7. Evals saves screenshot and uses for VisionJudge evaluation + +## Logging + +Logs are written to `logs/` directory (auto-created): +- `combined.log` - All log events +- `error.log` - Error events only +- `api.log` - API request/response logs + +## Docker Integration + +The eval-server runs inside `kernel-browser-extended` Docker container. + +Volume mount for live development: +```yaml +volumes: + - "./eval-server/nodejs:/opt/eval-server" +``` + +## Dependencies + +Core dependencies: +- `ws` - WebSocket server +- `uuid` - ID generation +- `winston` - Structured logging +- `dotenv` - Environment variable management + +Removed dependencies: +- ~~`openai`~~ - Not needed (evals handles judging) +- ~~`js-yaml`~~ - Not needed (evals handles YAML loading) + +## What This Server Does NOT Do + +- ❌ Load YAML evaluation definitions (handled by evals/) +- ❌ LLM-as-a-judge evaluation (handled by evals/) +- ❌ Test orchestration (handled by evals/) +- ❌ Result reporting (handled by evals/) +- ❌ Screenshot analysis (handled by evals/) + +## What This Server DOES Do + +- ✅ WebSocket server for browser agent connections +- ✅ JSON-RPC 2.0 bidirectional communication +- ✅ HTTP REST API endpoints +- ✅ CDP screenshot capture +- ✅ CDP page content retrieval +- ✅ CDP tab management +- ✅ Return metadata (clientId, tabId) for screenshot capture diff --git a/eval-server/nodejs/README.md b/agent-server/nodejs/README.md similarity index 90% rename from eval-server/nodejs/README.md rename to agent-server/nodejs/README.md index d29f9bc2b1..d9da46f7fc 100644 --- a/eval-server/nodejs/README.md +++ b/agent-server/nodejs/README.md @@ -350,6 +350,59 @@ curl -X POST http://localhost:8081/v1/responses \ -d '{"input": "What is 2+2?"}' ``` +#### `/v1/responses` Endpoint Details + +The `/v1/responses` endpoint provides an OpenAI-compatible interface for chat requests. **Each request automatically creates a new browser tab** to isolate the chat session. + +**Behavior:** +1. Finds a client with existing tabs (skips dummy clients with 0 tabs) +2. Opens a new browser tab via Chrome DevTools Protocol (CDP) +3. Waits for the tab's DevTools to connect (up to 10 seconds) +4. Executes the chat request in the isolated new tab +5. Returns the response in OpenAI Responses API format + +**Request Format:** +```json +{ + "input": "Your question or prompt here", + "model": { + "main_model": { + "provider": "openai", + "model": "gpt-4", + "api_key": "sk-..." + } + } +} +``` + +**Response Format:** +```json +[ + { + "id": "msg_...", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Response text here", + "annotations": [] + } + ] + } +] +``` + +**Requirements:** +- At least one DevTools client must have a connected tab +- Chrome must be running with remote debugging enabled (port 9223) + +**Benefits:** +- Each chat request runs in isolation +- Supports parallel requests in different tabs +- Better tracking and debugging per request +- Automatic cleanup via browser tab management + ## CLI Usage Interactive command-line interface for server management: diff --git a/agent-server/nodejs/clients/README.md b/agent-server/nodejs/clients/README.md new file mode 100644 index 0000000000..5c00064742 --- /dev/null +++ b/agent-server/nodejs/clients/README.md @@ -0,0 +1,41 @@ +# Client Configurations + +This directory contains client configuration files for the Browser Agent Server. + +## File Naming Convention + +Client configuration files **must** be named using the client's UUID: + +``` +{client-uuid}.yaml +``` + +Example: +``` +9907fd8d-92a8-4a6a-bce9-458ec8c57306.yaml +``` + +## Creating a New Client + +1. Copy `example-client.yaml` to a new file with your client's UUID: + ```bash + cp example-client.yaml {your-client-uuid}.yaml + ``` + +2. Edit the new file and update: + - `client.id` - Must match the filename (without .yaml extension) + - `client.name` - A friendly name for the client + - `client.secret_key` - Authentication secret (default: "hello") + - `client.description` - Description of the client + +## Auto-Discovery + +The server automatically discovers and loads all `.yaml` files in this directory on startup. The filename (without extension) is used as the client ID. + +## Security Note + +Client configuration files contain secret keys and are excluded from version control via `.gitignore`. Keep these files secure and never commit them to the repository. + +## Example Configuration + +See `example-client.yaml` for a template configuration file. diff --git a/agent-server/nodejs/clients/example-client.yaml b/agent-server/nodejs/clients/example-client.yaml new file mode 100644 index 0000000000..3356598584 --- /dev/null +++ b/agent-server/nodejs/clients/example-client.yaml @@ -0,0 +1,15 @@ +# Example Client Configuration +# +# To create a new client: +# 1. Copy this file and rename it to: {client-uuid}.yaml +# Example: 9907fd8d-92a8-4a6a-bce9-458ec8c57306.yaml +# 2. Update the client ID to match the filename +# 3. Set a secret key for authentication +# +# The filename MUST match the client ID for proper client discovery. + +client: + id: YOUR-CLIENT-UUID-HERE + name: My DevTools Client + secret_key: your-secret-key-here + description: Browser Operator DevTools client diff --git a/agent-server/nodejs/logs/requests.jsonl b/agent-server/nodejs/logs/requests.jsonl new file mode 100644 index 0000000000..d90e74a581 --- /dev/null +++ b/agent-server/nodejs/logs/requests.jsonl @@ -0,0 +1,11 @@ +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:21CE5F209B38C519F79D7090ADF02AD0","duration":251662,"evaluationId":"api-eval-1761045054379-mwi8e6d","name":"API Request","response":{"executionTime":251578,"metadata":{"evaluationId":"api-eval-1761045054379-mwi8e6d"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"Type \"DevTools automation\" in the search box and then click the \"Google Search\" button"},{"action":"tool","entity":"model","isFinalAnswer":false,"reasoning":null,"toolArgs":{"reasoning":"User asked to type 'DevTools automation' into the search box and click 'Google Search'. I will automate those interactions on the current Google page and report the result so the user can continue.","task":"On the Google homepage currently open, perform the following interactions: 1) Locate the main search input (the combobox or input field labeled 'Search' / role='search') and type the exact phrase: DevTools automation. 2) Locate and click the primary button labeled 'Google Search' to submit the query. 3) Wait for the results page to load and return a short status report indicating success or any errors encountered, and include the resulting page title and URL. \n\nFallback instructions if elements can't be found by label/text: try common selectors for Google search input (e.g., input[name='q'], input[type='search']) and for the search button try buttons with text 'Google Search' or input[type='submit']. If a click triggers navigation, wait up to 10 seconds for navigation to complete. If the page has an 'AI Mode' or other UI that intercepts the search, attempt the standard search submission by sending Enter to the input field.\n\nDo not request or rely on full accessibility trees from the user. Report any unexpected prompts (sign-in, dialogs) encountered."},"toolCallId":"a5e9298a-ff2a-4f2d-9f68-5d20496212a2","toolName":"web_task_agent","uiLane":"agent"},{"agentSession":{"agentDescription":"A specialized agent that controls the browser to navigate web pages, reads contents, and orchestrates site-specific web tasks. Takes focused objectives from the base agent (like \"find flights on this website\") and breaks them down into individual actions that are executed. Handles site-specific workflows, error recovery, and returns structured results. Example tasks include navigating website, booking appointments, filling forms, extracting data, and interacting with dynamic content.","agentDisplayName":"web_task_agent","agentName":"web_task_agent","agentReasoning":"User asked to type 'DevTools automation' into the search box and click 'Google Search'. I will automate those interactions on the current Google page and report the result so the user can continue.","config":{"description":"A specialized agent that controls the browser to navigate web pages, reads contents, and orchestrates site-specific web tasks. Takes focused objectives from the base agent (like \"find flights on this website\") and breaks them down into individual actions that are executed. Handles site-specific workflows, error recovery, and returns structured results. Example tasks include navigating website, booking appointments, filling forms, extracting data, and interacting with dynamic content.","handoffs":[],"maxIterations":15,"name":"web_task_agent","schema":{"properties":{"extraction_schema":{"description":"Optional schema definition for structured data extraction tasks.","type":"object"},"reasoning":{"description":"Clear explanation of the task objectives and expected outcomes.","type":"string"},"task":{"description":"The web task to execute, including navigation, interaction, or data extraction requirements.","type":"string"}},"required":["task","reasoning"],"type":"object"},"systemPrompt":"You are a specialized web task orchestrator agent that helps users with site-specific web tasks by directly interacting with web pages. Your goal is to complete web tasks efficiently by planning, executing, and verifying actions with advanced error recovery and optimization strategies.\n\n## Your Role & Enhanced Capabilities\nYou receive focused objectives from the base agent and break them down into individual actions. You coordinate between navigation, interaction, and data extraction to accomplish web tasks autonomously with:\n- **Dynamic content detection**: Recognize SPAs, AJAX loading, and async content\n- **Site pattern recognition**: Adapt strategies based on common website patterns\n- **Intelligent error recovery**: Handle rate limits, CAPTCHAs, and service issues\n- **State management**: Preserve context across complex multi-step workflows\n- **Data quality validation**: Ensure extraction completeness and accuracy\n\n## Available Context & Enhanced Understanding\nYou automatically receive rich context with each iteration:\n- **Current Page State**: Title, URL, and real-time accessibility tree (viewport elements only)\n- **Progress Tracking**: Current iteration number and remaining steps\n- **Page Updates**: Fresh accessibility tree data reflects any page changes from previous actions\n- **Network State**: Monitor for ongoing requests and loading states\n- **Error Patterns**: Track recurring issues for adaptive responses\n\n**Important distinctions:**\n- **Accessibility tree**: Shows only viewport elements (what's currently visible)\n- **Schema extraction**: Can access the entire page content, not just the viewport\n- **Dynamic content**: May require wait strategies and loading detection\n\n## Enhanced Guidelines\n\n### 0. Thinking Usage (CRITICAL)\n**ALWAYS use thinking tool:**\n- At the start of any task to create a grounded plan\n- After 3-4 actions to reassess progress\n- When encountering unexpected results or errors\n- Before major decisions (navigation, form submission)\n- When the page changes significantly\n\n**SKIP thinking tool when:**\n- On Chrome internal pages (chrome://*) - immediately navigate to a real website instead\n\n**Thinking provides:**\n- Visual confirmation of current state\n- High-level list of things to consider or work on\n- Current progress assessment toward the goal\n- Flexible observations about the situation\n\n### 1. Planning & Site Recognition\n**ANALYZE site patterns first**: Before executing tools, identify:\n- Site type (e-commerce, social media, enterprise, news, etc.)\n- Framework indicators (React, Vue, Angular, jQuery)\n- Loading patterns (SSR, SPA, hybrid)\n- Known challenges (auth walls, rate limiting, complex interactions)\n\n**PLAN with adaptability**: Create a flexible plan that accounts for:\n- Alternative paths if primary approach fails\n- Expected loading times and dynamic content\n- Potential error scenarios and recovery strategies\n- State preservation requirements\n\n### 2. Enhanced Execution Strategy\n**TAKE INITIAL SCREENSHOT**: Always take a screenshot at the beginning (iteration 1) and document the starting state\n\n**USE SMART WAITING**: After navigation or actions, intelligently wait for:\n- Network idle states (no pending requests)\n- Dynamic content loading completion\n- JavaScript framework initialization\n- Animation/transition completion\n\n**IMPLEMENT PROGRESSIVE LOADING DETECTION**:\n- Look for skeleton loaders, loading spinners, or placeholder content\n- Monitor for content height/width changes indicating loading\n- Check for \"load more\" buttons or infinite scroll triggers\n- Detect when async requests complete\n\n**TAKE PROGRESS SCREENSHOTS**: Document state at iterations 1, 5, 9, 13, etc., AND after significant state changes\n\n### 3. Advanced Error Recovery\n**RECOGNIZE ERROR PATTERNS**:\n- **Rate Limiting**: 429 errors, \"too many requests\", temporary blocks\n- **Authentication**: Login walls, session timeouts, permission errors\n- **Content Blocking**: Geo-restrictions, bot detection, CAPTCHA challenges\n- **Technical Issues**: 5xx errors, network timeouts, JavaScript errors\n- **Layout Issues**: Overlays, modals, cookie banners blocking content\n- **Chrome Internal Pages**: action_agent cannot interact with any Chrome internal pages (chrome://*) including new tab, settings, extensions, etc. - navigate to a real website first\n\n**IMPLEMENT RECOVERY STRATEGIES**:\n- **Rate Limits**: Use wait_for_page_load with exponential backoff (2s, 4s, 8s, 16s), then retry\n- **CAPTCHAs**: Detect and inform user, provide clear guidance for manual resolution\n- **Authentication**: Attempt to identify login requirements and notify user\n- **Overlays**: Advanced blocking element detection and removal via action_agent\n- **Network Issues**: Retry with different strategies or connection attempts\n- **Chrome Internal Pages**: If detected (URL starts with chrome://), immediately navigate to a real website using navigate_url\n\n### 4. State & Context Management\n**PRESERVE CRITICAL STATE**:\n- Shopping cart contents and user session data\n- Form progress and user inputs\n- Page navigation history for complex flows\n- Authentication status and session tokens\n\n**IMPLEMENT CHECKPOINTING**:\n- Before major state changes, take screenshot to document current state\n- After successful operations, confirm state preservation\n- Provide rollback capabilities for failed operations\n\n### 5. Data Quality & Validation\n**VALIDATE EXTRACTION COMPLETENESS**:\n- Check for required fields in extraction schema\n- Verify data format matches expected patterns\n- Confirm numerical values are within reasonable ranges\n- Detect partial or truncated content\n\n**IMPLEMENT QUALITY SCORING**:\n- Rate extraction success based on completeness\n- Identify missing or low-confidence data\n- Retry extraction with alternative methods if quality appears insufficient\n\n### 6. Performance Optimization\n**OPTIMIZE TOOL USAGE**:\n- Use direct_url_navigator_agent for known URL patterns first\n- Batch similar operations when possible\n- Use most efficient extraction method for content type\n- Avoid redundant tool calls through smart caching\n\n**MANAGE LARGE CONTENT**:\n- For large pages, extract in targeted chunks using extract_data\n- Use CSS selectors to limit extraction scope when possible\n- Implement pagination handling for multi-page datasets\n\n### 7. Enhanced Communication\n**PROVIDE PROGRESS UPDATES**:\n- Report major milestones during long operations\n- Explain current strategy and next steps clearly\n- Notify user of encountered obstacles and recovery attempts\n- Clearly communicate task completion status\n\n**HANDLE USER INTERACTION**:\n- Identify when user input is required (CAPTCHAs, 2FA, manual authorization)\n- Provide clear instructions for user actions\n- Resume execution smoothly after user intervention\n\n## Task Execution Framework\n\n### Phase 1: Analysis & Planning (Iterations 1-2)\n1. **Sequential Thinking**: USE sequential_thinking tool at the start to analyze the current state and create a grounded plan\n2. **Site Pattern Recognition**: Identify website type and framework from the visual analysis\n3. **Initial Screenshot**: Already captured by sequential_thinking\n4. **Strategic Planning**: Follow the plan from sequential_thinking output\n\n### Phase 2: Execution & Monitoring (Iterations 3-12)\n1. **Progressive Execution**: Execute plan from sequential_thinking step by step\n2. **State Monitoring**: After major changes or unexpected results, use sequential_thinking again to reassess\n3. **Error Detection**: When actions fail, use sequential_thinking to understand why and plan recovery\n4. **Quality Validation**: Continuously verify extraction quality\n\n### Phase 3: Completion & Verification (Iterations 13-15)\n1. **Final Validation**: Confirm task completion and data quality\n2. **State Cleanup**: Handle session cleanup if needed\n3. **Results Formatting**: Structure output according to requirements\n4. **Completion Documentation**: Final screenshot and summary\n\n## Advanced Tool Usage Patterns\n\n### Smart Navigation Strategy\n1. Try direct_url_navigator_agent for known URL patterns\n2. Use navigate_url for standard navigation\n3. Implement wait_for_page_load for dynamic content\n4. Apply scroll_page strategically for infinite scroll\n5. Use take_screenshot for understanding the web page state\n\n### Dynamic Content Handling\n1. After navigation, use wait_for_page_load (2-3 seconds) for initial load\n2. Check for loading indicators or skeleton content\n3. Use wait_for_page_load until content stabilizes\n4. Re-extract content and compare with previous state\n5. Repeat until content stabilizes\n\n### Error Recovery Workflow\n1. Detect error type through page analysis\n2. Apply appropriate recovery strategy with wait_for_page_load\n3. Document recovery attempt in screenshot\n4. Retry original operation with modifications\n5. Escalate to user if automated recovery fails\n\nRemember: **Plan adaptively, execute systematically, validate continuously, and communicate clearly**. Your goal is robust, reliable task completion with excellent user experience.\n","temperature":0.3,"tools":["navigate_url","navigate_back","action_agent","extract_data","node_ids_to_urls","direct_url_navigator_agent","scroll_page","take_screenshot","wait_for_page_load","thinking","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:11:04.693Z","metadata":{"handoffs":[]},"name":"web_task_agent","promptHash":"a303049e0949e112232a96e37801282f80156a05b342668d3d77125c9cd9e910","toolsetHash":"93f1669ae62f6114610b73150637fb36266b8b4f41a689a2238a14fb6cb84aec","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:15:03.021Z","iterationCount":8,"maxIterations":15,"messages":[{"content":{"text":"User asked to type 'DevTools automation' into the search box and click 'Google Search'. I will automate those interactions on the current Google page and report the result so the user can continue.","type":"reasoning"},"id":"566a8776-1808-4851-bed0-6fed6c74f561","timestamp":"2025-10-21T11:11:04.696Z","type":"reasoning"},{"content":{"toolArgs":{"context":"Page title: Google. Visible accessibility tree shows a combobox: 'Search' (node [2]) and a button: 'Google Search' (node [394]). Follow fallbacks if elements can't be found; send Enter if needed. Take initial screenshot and plan actions.","userRequest":"On the Google homepage currently open, type 'DevTools automation' into the main search input and click the 'Google Search' button to submit. Wait for results and report status, title, and URL."},"toolCallId":"25cc4a9b-7162-42d9-91bb-2dd594d4dbcd","toolName":"thinking","type":"tool_call"},"id":"0a21ac9b-f66c-4dd8-8810-c07de1e91be7","timestamp":"2025-10-21T11:11:18.509Z","type":"tool_call"},{"content":{"result":{"currentProgress":"Ready to proceed. Elements needed are present in the accessibility tree (combobox [2] and 'Google Search' button [394]), but no actions have been performed yet. Plan is to take an initial screenshot then input the query and submit.","observations":"The combobox role indicates an input-like control rather than a plain text node, so typing should work. The button labeled 'Google Search' is available and is the preferred submit target. There are nearby controls (voice/image/AIMode) that might intercept focus or interaction; ensure focus is explicitly set on the combobox before typing. Also be prepared for Google autocomplete suggestions which may appear; hitting Enter should still submit if the input is focused. Expect the results page title to update (commonly 'DevTools automation - Google Search') and the URL to include the query parameter q=DevTools+automation.","thingsToDoList":["Take an initial screenshot of the current page (before interacting).","Locate and focus the combobox node [2] labeled 'Search'. Verify it is editable (input-like) and clear any prefilled text.","Type or set the value 'DevTools automation' into the combobox. If direct value setting is possible, send appropriate input/event notifications so the page registers the text (e.g., input/keydown/keyup events).","Primary submission: click the 'Google Search' button (node [394]).","Fallback submission: if clicking the button fails to submit, send an Enter/Return key event while the combobox is focused.","Wait for navigation / results to load. Monitor for a change in the document title and/or URL (expect a URL containing '/search?q=DevTools+automation'). Also optionally wait for search-results-specific elements to appear (result list or result stats).","Take a screenshot of the results page (post-submission).","Report final status (success or failure), the resulting page title, and the resulting URL. If failed, include error details and which fallback was attempted."],"visualSummary":"The page is the Google homepage. The main search combobox is present (node [2], accessible name: 'Search') inside a search region. The primary 'Google Search' button is present (node [394]). There are other controls nearby (voice search [229], image search [233], 'I'm Feeling Lucky' [397], and an 'AI Mode' link). Footer links and header navigation are present but not relevant to the search action."},"success":true,"toolCallId":"25cc4a9b-7162-42d9-91bb-2dd594d4dbcd","toolName":"thinking","type":"tool_result"},"id":"ef7d78a9-f6eb-46de-9259-ff51cdcecb84","timestamp":"2025-10-21T11:11:32.232Z","type":"tool_result"},{"content":{"toolArgs":{"fullPage":false,"reasoning":"Initial screenshot of Google homepage before interaction (iteration 1) as required by enhanced execution guidelines."},"toolCallId":"0aeb2d10-886d-46b9-9d7b-fbd28e9c24fb","toolName":"take_screenshot","type":"tool_call"},"id":"52d7b5cf-affc-4bb5-a59a-3644cef4865c","timestamp":"2025-10-21T11:11:33.836Z","type":"tool_call"},{"content":{"result":{"imageData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABVUAAAPhCAIAAAB2c9ixAAAQAElEQVR4nOzdB3wUxd/H8bmWnpAECL2D9I4ixQKINBXpTRSQKirYK/wVCwiigqJ0BJEivYMgooKAIr1I7xAIkJCeq88vnM8ZQ3LpIWw+b/PCvd3Zu92r+52ZnTU6HA4FAAAAAAA0zagAAAAAAIDWkf8BAAAAANA+8j8AAAAAANpH/gcAAAAAQPvI/wAAAAAAaB/5HwAAAAAA7SP/AwAAAACgfeR/AAAAAAC0z6gAAAAAAMhnwiKtvx6K/utE7Kkr5rNh5rNXEyLj7CoPCPDWlwnxLBviUS7Eo35Fn2Y1/YP9DCo76BwOhwIAAAAAIB8IDbd+sz5s/e7IQ+fj1V2idjnvVnUCBj5aqGhQlprwyf8AAAAAAO2T5P/5yqszNl2Lt9yVKdjLpOvfstDwx0MyXQtA/gcAAAAAaNndnvyTctYCvNqhSCH/DJ8UQP4HAAAAAGiW2Wp/7dtLMzZdVxry7CMFx/Up7mHUZ2gt8j8AAAAAQJuuR1m7jju981is0pyG9/j88Fq5gv4ZOBcgY7UFAAAAAADcFSxWR5ex2gz/Qvar+6dnZB/Tvwr5HwAAAACgQa/MuvjHcW2Gf6ftR2NkH9NfXq8AAAAAANCW6Ruvz/xJU+f8p0j2UfY0nYU5/x8AAAAAoClXIizVXjiScPeP9p8eXibdwYlViwaZ0ixJ+z8AAAAAQFM+WnQln4R/EW9xfLbyanpKkv8BAAAAANpx+krCt5u13/M/qZmbroeGW9IsloFLBQAAAAAAkFG79+xbtGSZ0um6d+1Uu2aNdM7JtNFLrtjz2Wnu8RbHx4uvTBxQ0n0x2v8BAAAAADlozvcLLodeuXw5dO73C51zvv3u+zTnZNqfWR7zv8eDQVs+rHR1ds0dYyuP6lHMxzMxON9b0efMtBqVS3iqLPhueNk0U3rm/HY4Os0y5H8AAAAAQA7S63XOCdfw80ajMc05mXP0YsLxywkqC1rU8v9mUOltf8cMm35h/e7I7g8ErXi7vMw/cTlh9ubrZ6+aVRas/CNC7lPlANlr2Xf3ZQzvvfeeygFHjx5t0KBBtWrVKlSo4JrZsWPH8PBwma/yBrvdPn369FGjRo0ePfqPP/7Q6XRVqlRRAAAAAPKZyMjIDz74YMyYMWPHjl21atWpU6d8fHxKlCihMuvNN988ePDg/fff/9Zbbx06dEgmMrS6bMaSJUtatWqlNKFwoUJnzp0LCgrs0b1z0SIh6ZyTObN/vrHlYNot4W689ESIVEE8/cXZg+fi5a7OhpmHPx6ybnfk6atmuWm1q6w4fD4+i9UTbpQoaGpcxddNgZw6/3/t2rWenp7y76OPPqqym3wMevXq9fTTT6usmTFjxnfffTdgwIDixYvv3r373XffvXjx4nPPPRcWFnbvvfdu2LChcuXKCgAAAICmJSQk9OjR45577hk/frwzAixYsGDgwIHr1q0rWrSoyhSpSlD4f/Xq1pa/jM7JnP1n4lSWhRQweZl08beuIPDjnsjun565eN3SpIrvhvcqVhl6+MJ1i6+nftKgUq3rBYRH2yatDRvzdPGH3jl++krCuek1hkw+/3y7wuWLeP56KGro1POh4dak97zynQqh4ZaBX5/r3jRobJ8SI+ddeqtTUT9vw4Lfbrwy66LKmjT3Pafy/7JlyyRIT5kyRT5LUhGg8iT5PHfu3Ll3794y3aJFC5PJJNUBstkKAAAAQL4xdepUo9E4btw4Vxd0aWts0qRJoUKFZFoa8IODg3ft2qXT6RwOx+uvv/7hhx96eXkVK1bsnXfeKViwoDTvz5o165dffpHpBx54QFr+DQaD/Ct1B8OHD1dZIy2Ubdq0efXVV1esWBEfH9++fXuz2bxlyxYPDw+ps3j88celzLRp0yTanDp1qk6dOkOGDGnYsKHMnDt3rqSbwoULV61a9cSJE7Lik08+eenSJanjOHnypNxJjRo1Ro0aJTsiuyYlIyIiJLu1bNmyX79+sqfqrnUlIu1h8N2buuFax/sDfxpVaeZP11f+cTMs0rp6102ZX6nYv8H2w6eKP35vga/Whp0MTRjatnDS1V99sojcg9lqf+XJIu90LvrCtAupPVABH0PPB4M/XBRaPNj0dueifx6PXbA1XGVBmvueI+f/79u37/r164MGDQoJCZFW9KSLQkND+/btW7169U6dOh08eNA58+bNm/IRatasmbN7TFRUlLrVA6ds2bLyWXKW+eyzz6TNX27KzKNHj44cObJjx44qyy5c+PfFkIoAeRT56Erjv7rVy2D06NGpbV5MTIxsyYQJE+R7Qb4pZM7GjRvla6JWrVrPPPPM+fPnFQAAAIC7wR9//NG6dWtX+HeqUKGCc440E0qBb775Zv78+RKbP//8c4kM33//vaSSn3/+WQrIHD8/vx07dixYsGD9+vU//fSTyj7y6NHR0b6+vkuWLJHahC+//LJ48eKLFy/u2bOnhBcpsHv3bqm/GDNmzN69eyWwOCPMmTNnPvjgg08//VRqAUqVKrVz505npH/++eeLFCnyww8/yKZKHJOKA5kpNQJt27adPXv2nDlzZMUjR46o7LZ7z7633n3vrRHv7ztwMP1zMudsWJbOzxcHz8U//O7x01fNY54ucfirah89VdxkSF4h0qlR4OT11/43//Kcn2/0/+pc0kWjF4dOWhc2beP1NbtuVivl7f6xen9x5rstNz5ZeuXg2biaZdMonKYrEVb3BXIk/69evVrSstQkyadozZo1SRctWrSoadOm8g6z2+1SQWC1Jm7fG2+8sXXr1pkzZ0q1k1Q+vfbaa6nds1QcyDuycuXKUlO1dOlSlTWDBw+WT5E8utSf2Ww2qa5r1KiRVE/8+eefslRqLiTtu988yfxvv/12t27dpGJCavjkQyif9rp16/bv3z+Lo1YAAAAAyB3S9B0UFOSclrbx8v/PeeQvyVmO8IODg2XinnvukenAwEC9Xi/TsqIUmD59+nvvvSdt/jK/WrVqZ8+eVdnHmdudZ1VXqVJFmiEfe+wx5/SNGzdkol69epJfZGNkunHjxqdOnZIJmVPzFpl+6qmnnB0ZpJFS6ggk73h4eAQEBMj8bdu2yXwJbqtWrdq/f7/Ml1oD2QWV3XJz/P80M3B6nLic8NTnZyoMOjhmSWjfFgWXvlU+6dJAX0Own+HX/x9vP8HynyEBwiL/2YB4s8N54YDU2OwO19bGWezuC6dHmu3/2d//X3KvvHucyVnepj169JD28wIFCjiXSuR+9tlnZaJ27doStn///feKFStKJZnUP5UrV07myydH2vnlM+P6BOYc2Typ+vr6669lk6RWTAK8VFgkK3Pp0qUUN8/5ERoxYoSzd83IkSO7dOniXF2q5aTybM+ePfJRVAAAAADyNokeziytbjX7OyP0lClTTpw44Zwpwdg5IWk86bQ0IsqENAFKrJCWwvDwxM7bOZECnA/qrAtwTTsbUyMjI7/66itp4T9w4IDc9PZObEOWagJ/f3/nulJVERIS4pwp/zrzi1OZMmXUra7W0vI/evToffv2de/e/d1335VVVLbKzfH/vUw6szVL99CqbsCxS/Gnr5gj4+zjV1w9dilh/itl65b/t3HeuTNW293X4pv97f87duyQ932LFi3Urbe+1JNJfnYtNZlMzolixYpJ8pcqKHmTSYWTazxMmZCbzo9cLqhfv/6MGTNkm1u1ajV06NBZs2YlK+B+81y7I8UmT55c9v/JM5C91X4AAAAAckjz5s1Xr15tNmem37jE7xdffLFdu3bS5C4xwZmDctPUqVMPHz4sYUQe3dVF2tfXNzY21lUmLCxM/nXWAkhyOfX/nOcvSPXHsGHD5s+fv3Hjxt27dy9ZskRlt6d6dCtSJKRkyRK9n+qe/jmZUyTQpLLms34l3uhQxHXz0LnEQfWCfA2uOeExNvmrWeafGgFPU450q8+ENPc9+9v/165dm5CQUKNGDdecNWvWdOvW7faSUmVlMBik+irpmTZS1SQznRVpOUpqv/bv3y81FJ6enoULF37rrbd8fHzkw9O3b9+kxdK5eVJH1adPn6TX5yhfvrwCAAAAkOf16NFj0aJFw4cPf+2115zdfjdv3vz999/36tUrzXWvX78eFxcnsV+Swk8//bRnz546deqoXHT58uVatWpJ82pUVJTEGdstDRo0GDlypNQLVKtWbcGCBVevXpWS0jTbsmVLafKUCgu5KROSg2RO27Ztp0+fLjteokQJf39/Z7eC7JWb4/8XCTRm8QJ732258U7nopfCLbtPxun16plmwdeibLtOxLoCv1j8e8Tg1oUuXDf7eemHtCms8gbZd/cFsjn/y1tt3bp18n5q1KiRc87p06dHjBghFU7y3pKbrjeT1JOdOXOmUqVKRYsWjY6OlmlpNpf5R48elWReu3ZtSeOStM+fP1+9enWZn7naODfkQeVzPmXKFFdol3e8bFWyYlWqVElx85IVq1ChgtR6uPZaPnuu/jYAAAAA8jJpEZTW708++eTJJ590jvbdtGnTcePGJe0qnxoJEd27d5cVpeFQGkGHDh0qTfG5eR1xaYYcMmSItOpfvHjx008/lQqIAQMGzJw5U5pge/bsWaZMmWbNmkkFgbOwFHj//fc7dOgg6Uay23vvvSf7/uGHHw4bNqxAgQJ//fVX69atZXfU3Szr7f9jllyJjLW1rB0w4NFC8Wb773/HPPbBici4/5zkP3LepUBfw6RBpa5EWOduuVG9W1Gb/c6fDpDmvuuyd5i6n3/+Wd5t8r5xnfAv9y+pWN6RzzzzTMeOHSXPf/bZZ9Kivnz58j/++EPq1XQ63XPPPeccY99ut7/77rtSKTBq1ChZt3PnzvHx8YMHD5a1JkyYUL9+famEk/lyP1WrVu3SpUsWq9bkw7lr1y55xEKFCskDff7556VKlZo0aVJsbKzUk33wwQdNmjSRZvwUN09qAaRiYunSpc7Tew4cOCCfealje/zxx3/88Ue5q9WrVzurPAAAAADgDpKQ8tprrz344IPqDtm9Z9+iJcuUTte9a6faNWukc07mjF9x9X/zL6scJuFfqgbiLYlp+tE6/kvfLF/q2YPhMTnejd2993sUe6V9iJsC2Xyiwpo1ax566CFX+Fe3Ovk/9thjrqsASC3UtGnT+vfvf/Lkya+++so5gsWYMWNKliwpab9FixaSwCVFOwtLzPb19X3rrbekpiBp3xuZloqAiRMnqqwZPXq0bMnixYsHDRo0fvz45s2bjx07Vub7+PhIpZdUjK1du9bN5iVVs2bN6dOnS6WG1BTOmDFDPl2EfwAAAAB3RFRUlLRl/vLLLzK9ffv20NBQVxeAOyI3x/9/rEEBlfPe6Fjkx/crNa7i+3ANv3F9Sv51MvaOh3+Vjn3XcZk6AAAAANCYFStWzJs3T+Kes8O1NNOqO+fl19++cSPx4giFChb89JMP0zkn0+q+9HcWhwBIU4WiHpOHlG5U2Vem5bF6jj9z5EK89p6TwAAAEABJREFUuqMqFfPc83kV92XI/wAAAACAHLR7z76Fi5eaTKYunZ509fZPc06mvT774tfrrql85qUnQj7oWcx9GfI/AAAAAEA7zlxNqP/K0QRLPoq6nibd319VLVwgjfH/8sqFCgEAAAAAyLqyIZ7P5Zlr8uUO2d80w7+i/R8AAAAAoDE3Y21Vhx5OdtE+rfL31h+dVC3Ax5BmSdr/AQAAAACaUsDH8MWzJVX+8NWAUukJ/4r8DwAAAADQnq5Ng156IkRpnexjp8aB6SxM/gcAAAAAaND73Ys2qeKrtKtlbX/Zx/SXJ/8DAAAAADRIr9fNfalMyYJpD4x3NypR0PTtsDKyj+lfhfwPAAAAANCmwgVM+ydUefaRgkpbnmkWfGBClQLpO+3fhfH/AQAAAAAaN33jtZdnXrTf/fFX2vsnDijZp3lmajTI/wAAAAAA7Tt2KWHC6qvzfgm32O7KFGw0qB4PBL/0RMg9xT1VppD/AQAAAAD5xcXrlvErrs7++XqC5a7Jwj6e+n4tCr7QrnCJrI1lQP4HAAAAAOQ7O4/FLt0esetkTESMLTzadjPWlkdqBDxNugI+hkBfQ5Cf4f7Kvh0aBjao6KOyg478DwAAAACA5hkVAAAAAADQOvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf+R8AAAAAAO0j/wMAAAAAoH3kfwAAAAAAtM+oAAAAAACA1pH/AQAAAADQPvI/AAAAAADaR/4HAAAAAED7yP8AAAAAAGgf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2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+BwAAAABA+8j/AAAAAABoH/kfAAAAAADtMyoAAAAAAKB15H8AAAAAALSP/A8AAAAAgPaR/wEAAAAA0D7yPwAAAAAA2kf+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"},"success":true,"toolCallId":"0aeb2d10-886d-46b9-9d7b-fbd28e9c24fb","toolName":"take_screenshot","type":"tool_result"},"id":"10311104-c430-41f6-aa28-772cf279516c","timestamp":"2025-10-21T11:11:33.951Z","type":"tool_result"},{"content":{"toolArgs":{"hint":"Ensure the combobox is focused before typing; if it isn't editable, try common input selectors (input[name='q'], input[type='search']) as a fallback.","input_data":"DevTools automation","objective":"Focus the main search combobox labeled 'Search' and type the phrase 'DevTools automation' into it.","reasoning":"The accessibility tree shows node [2] is a combobox labeled 'Search' — this is the main Google search input. Focusing it first ensures typing goes to the correct element and avoids interference from other UI elements like AI Mode or voice search."},"toolCallId":"5ce68802-c058-488b-bb35-a98075130e25","toolName":"action_agent","type":"tool_call"},"id":"855dd589-9f80-45e7-b4a0-49144c7e042a","timestamp":"2025-10-21T11:11:40.246Z","type":"tool_call"},{"content":{"context":{"hint":"Ensure the combobox is focused before typing; if it isn't editable, try common input selectors (input[name='q'], input[type='search']) as a fallback.","input_data":"DevTools automation","objective":"Focus the main search combobox labeled 'Search' and type the phrase 'DevTools automation' into it.","reasoning":"The accessibility tree shows node [2] is a combobox labeled 'Search' — this is the main Google search input. Focusing it first ensures typing goes to the correct element and avoids interference from other UI elements like AI Mode or voice search."},"nestedSessionId":"a1fbaf10-3747-4c12-9d83-2ed2ea3a9398","reason":"Handing off to action_agent","targetAgent":"action_agent","type":"handoff"},"id":"29cdf983-b4ec-48ba-a830-57732d5c51aa","timestamp":"2025-10-21T11:11:40.247Z","type":"handoff"},{"content":{"result":{"agentSession":{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"The accessibility tree shows node [2] is a combobox labeled 'Search' — this is the main Google search input. Focusing it first ensures typing goes to the correct element and avoids interference from other UI elements like AI Mode or voice search.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:11:40.248Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:12:36.631Z","iterationCount":3,"maxIterations":10,"messages":[{"content":{"text":"The accessibility tree shows node [2] is a combobox labeled 'Search' — this is the main Google search input. Focusing it first ensures typing goes to the correct element and avoids interference from other UI elements like AI Mode or voice search.","type":"reasoning"},"id":"2a14e0be-0724-4dd3-bd46-c0433a7307b0","timestamp":"2025-10-21T11:11:40.251Z","type":"reasoning"},{"content":{"toolArgs":{"method":"click","nodeId":2,"reasoning":"Focus the main search combobox labeled 'Search' by clicking on the combobox element to ensure it receives keyboard input."},"toolCallId":"a9f80474-1f3e-48e8-a6bf-cb5e35b324c3","toolName":"perform_action","type":"tool_call"},"id":"e9c53be2-d616-4842-80b6-d680d786d47d","timestamp":"2025-10-21T11:11:47.296Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [253] div"," [269] div"," [272] div"," [859] StaticText: Related to recent searches"," [536] listbox"],"hasChanges":true,"hasMore":{"added":true,"modified":false,"removed":false},"modified":[],"removed":[],"summary":"19 added, 0 removed, 0 modified"},"visualCheck":"- What changed in the page content\n - The page shows the Google search area with a combobox labeled “Search” present in the DOM.\n - A listbox with related search options is visible, along with the standard Google Search and I’m Feeling Lucky buttons.\n - The specific target in the action (a textarea inside the combobox) is not visible in the provided page structure.\n\n- Did the action appear to succeed?\n - It’s not clearly verifiable from the static content. There is no explicit indication that focus moved to the combobox. The combobox element exists, but there’s no visible focus state or input change shown.\n\n- Any error messages or unexpected behavior\n - No errors or unexpected behavior are shown in the content.\n\n- Overall assessment\n - Unclear whether the click succeeded in focusing the combobox. The DOM element referenced by the action (a textarea inside the combobox) does not appear in the provided view, suggesting a mismatch between the target and the actual element, or that focus state is not reflected in this static dump. To verify, check whether the active element is the combobox after the click or try typing into the search field to confirm input capture. If possible, adjust the selector to the actual input/combobox element and re-run the action.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"a9f80474-1f3e-48e8-a6bf-cb5e35b324c3","toolName":"perform_action","type":"tool_result"},"id":"1fcf77c0-23ab-4842-9d01-b49765b88b19","timestamp":"2025-10-21T11:12:01.519Z","type":"tool_result"},{"content":{"toolArgs":{"args":{"text":"DevTools automation"},"method":"fill","nodeId":2,"reasoning":"Type the phrase into the main search combobox (node 2) to set the query for Google search."},"toolCallId":"623c725c-9d5a-461f-a21d-717d5ba67900","toolName":"perform_action","type":"tool_call"},"id":"fdcae125-d403-480e-a2d9-1441aeb0ed99","timestamp":"2025-10-21T11:12:09.570Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [222] button: Clear"," [536] listbox"," [1049] span"," [1070] StaticText: chrome "," [1071] StaticText: devtools automation"],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":false},"modified":[" [859] StaticText: Related to recent searches → [931] StaticText: DevTools automation"," [548] option: chrome devtools recorder → [580] option: chrome devtools automation"," [580] option: chrome devtools → [548] option: devtools automation"," [612] option: @wdio/devtools-service → [644] option: what are the automation tools in devops"," [644] option: ⤕āĨā¤°āĨ‹ā¤Ž → [612] option: axe devtools automation"],"removed":[" [272] div"," [536] listbox"," [804] option: chrome beta"],"summary":"30 added, 3 removed, 10 modified"},"visualCheck":"- Changes observed:\n - The main search combobox (node 2) now shows the input value \"DevTools automation\" (StaticText under the combobox reflects the filled text).\n\n- Action success:\n - The fill action appears to have succeeded. The page content shows the combobox containing the expected value and the verification status states the fill action was verified successfully.\n\n- Errors or unexpected behavior:\n - No errors or warnings are present. The available options beneath the combobox include various variants (e.g., \"devtools automation\", \"chrome devtools automation\"), which is typical for a suggestions list.\n\n- Overall assessment:\n - The action succeeded: the phrase was entered into the main search combobox as intended, and the UI reflects the updated value. Ready to proceed with the search if desired.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"623c725c-9d5a-461f-a21d-717d5ba67900","toolName":"perform_action","type":"tool_result"},"id":"0a7fd2d0-ec08-4cdf-a69c-ce4436a0d646","timestamp":"2025-10-21T11:12:19.908Z","type":"tool_result"},{"content":{"answer":"Action completed.\n\n- What I did:\n - Focused the main search combobox (nodeId 2) and typed the phrase: DevTools automation.\n\n- Result:\n - The input now shows the value \"DevTools automation\" in the combobox. The pageChange evidence indicates the fill action succeeded and the UI updated accordingly.\n - There are related-suggestions beneath the combobox (e.g., devtools automation, chrome devtools automation).\n\n- Next steps (choose one):\n - Submit the search by pressing Enter in the combobox:\n - perform_action: { method: \"press\", nodeId: 2, \"args\": [\"Enter\"] }\n - Or click the Google Search button to execute the search:\n - perform_action: { method: \"click\", nodeId: 382 } (the visible \"Google Search\" button in the main area)\n\nWould you like me to submit the search now or keep refining the input first?","type":"final_answer"},"id":"f47ef0d2-5aeb-41ee-8817-349770ae52d0","timestamp":"2025-10-21T11:12:28.855Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"6130b296-3f4b-4971-af1f-677da09589f8","sessionId":"a1fbaf10-3747-4c12-9d83-2ed2ea3a9398","startTime":"2025-10-21T11:11:40.249Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"output":"Action completed.\n\n- What I did:\n - Focused the main search combobox (nodeId 2) and typed the phrase: DevTools automation.\n\n- Result:\n - The input now shows the value \"DevTools automation\" in the combobox. The pageChange evidence indicates the fill action succeeded and the UI updated accordingly.\n - There are related-suggestions beneath the combobox (e.g., devtools automation, chrome devtools automation).\n\n- Next steps (choose one):\n - Submit the search by pressing Enter in the combobox:\n - perform_action: { method: \"press\", nodeId: 2, \"args\": [\"Enter\"] }\n - Or click the Google Search button to execute the search:\n - perform_action: { method: \"click\", nodeId: 382 } (the visible \"Google Search\" button in the main area)\n\nWould you like me to submit the search now or keep refining the input first?","success":true,"summary":{"content":"1) User Request\n- Focus the main search combobox labeled \"Search\" and type the phrase \"DevTools automation\" into it, using fallback selectors if needed.\n\n2) Agent Decisions\n- Attempted to focus the combobox by clicking the element (nodeId 2).\n- Evaluated whether focus occurred; noted potential mismatch (the target appeared to be a textarea inside the combobox and not clearly showing a focused state.\n- Proceeded to input the text into the combobox (node 2) despite initial uncertainty.\n- Monitored DOM changes and confirmed the input value appeared as \"DevTools automation\" and the suggestions/listbox updated accordingly.\n- Acknowledged that the action succeeded in typing the text and provided next-step options to submit the search (Enter or click the Google Search button).\n\n3) Final Outcome\n- The phrase \"DevTools automation\" was successfully entered into the main search combobox.\n- The UI reflected the input and displayed related suggestions.\n- Next, the agent can either press Enter or click the search button to execute the search.","type":"completion"},"terminationReason":"final_answer"},"success":true,"toolCallId":"5ce68802-c058-488b-bb35-a98075130e25","toolName":"action_agent","type":"tool_result"},"id":"d3009236-039a-4e7b-85a5-52e177138b4c","timestamp":"2025-10-21T11:12:36.631Z","type":"tool_result"},{"content":{"toolArgs":{"context":"Previous action typed the text into the combobox. Visible 'Google Search' button node [394]. Will click that and wait up to 10 seconds for navigation. If click fails, fallback to sending Enter. Check for dialogs or sign-in prompts.","userRequest":"Submit the search by clicking the 'Google Search' button and wait for navigation/results to load, then report status with title and URL."},"toolCallId":"5bccf3e8-15fa-4955-9ff6-495700bf8b26","toolName":"thinking","type":"tool_call"},"id":"94ad84f6-9c9b-4854-a0e1-fe86cddea602","timestamp":"2025-10-21T11:12:41.356Z","type":"tool_call"},{"content":{"result":{"currentProgress":"The query text has already been typed into the combobox. The visible 'Google Search' button (node 394) is present and ready to be clicked. A suggestions listbox is open beneath the combobox which might slightly overlap UI.","observations":"There are two 'Google Search' buttons — one inside the suggestion area (node 382) and the main page button (node 394). The suggestions listbox (node 536) is open and could intercept clicks if it overlays the main button. No obvious modal dialogs or sign-in overlays are present in the accessibility tree, though a Sign in link exists in the header. Be prepared to fallback to sending Enter if clicking is blocked.","thingsToDoList":["Confirm the combobox (node 2) has the intended query text ('DevTools automation') and is focused if necessary.","Ensure the target 'Google Search' button (node 394, the main center button) is visible and enabled. If it appears covered by the suggestion list, consider closing suggestions first.","Attempt to click the visible 'Google Search' button (node 394).","After the click, wait up to 10 seconds for navigation or results to load. Detect success by either a URL change, a document title change, or the presence of a search-results container (if present in the updated accessibility tree).","If the click fails (button not actionable, covered, or no navigation/results within timeout), fallback to sending Enter/Return to the combobox to submit the search.","During and after submission, watch for dialogs or sign-in prompts (e.g., modal elements or focus-stealing overlays). If such a dialog appears, report it and stop or attempt a reasonable dismissal if automated handling is acceptable.","After successful navigation or results load, read and report the page title and current URL.","If nothing changes (no navigation and no visible results update), report a failure with relevant diagnostics: whether the button was disabled, whether suggestions were open, and whether a click or Enter was attempted."],"visualSummary":"This is the Google home page. The search combobox (node 2) already contains the text 'DevTools automation'. There are visible 'Google Search' buttons in two places (node 382 inside the suggestions area and node 394 in the main center area). A suggestions listbox (node 536) is open under the combobox. Page also shows top-nav links (Gmail, Images), a Sign in link, and footer links. No modal dialog nodes are visible in the accessibility tree."},"success":true,"toolCallId":"5bccf3e8-15fa-4955-9ff6-495700bf8b26","toolName":"thinking","type":"tool_result"},"id":"550a21c2-0896-4e81-9e98-b1928bff5e75","timestamp":"2025-10-21T11:12:56.636Z","type":"tool_result"},{"content":{"toolArgs":{"hint":"If the suggestions listbox is blocking the button, try clicking the other visible 'Google Search' button (node 382) or send Enter to the input field as a fallback.","input_data":"394","objective":"Click the primary 'Google Search' button (node 394) to submit the query 'DevTools automation'.","reasoning":"User requested clicking the primary 'Google Search' button. Node 394 corresponds to the center page 'Google Search' button visible in the accessibility tree."},"toolCallId":"50790563-1be1-4625-8ad3-76095dd99f46","toolName":"action_agent","type":"tool_call"},"id":"4c8780a9-50c2-4e66-bdde-84e98034f68e","timestamp":"2025-10-21T11:12:59.589Z","type":"tool_call"},{"content":{"context":{"hint":"If the suggestions listbox is blocking the button, try clicking the other visible 'Google Search' button (node 382) or send Enter to the input field as a fallback.","input_data":"394","objective":"Click the primary 'Google Search' button (node 394) to submit the query 'DevTools automation'.","reasoning":"User requested clicking the primary 'Google Search' button. Node 394 corresponds to the center page 'Google Search' button visible in the accessibility tree."},"nestedSessionId":"5fe5b519-6308-421b-b798-c977116f75e5","reason":"Handing off to action_agent","targetAgent":"action_agent","type":"handoff"},"id":"29882a13-ca0f-40ac-a9c4-db31f3b0b6e6","timestamp":"2025-10-21T11:12:59.591Z","type":"handoff"},{"content":{"result":{"agentSession":{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"User requested clicking the primary 'Google Search' button. Node 394 corresponds to the center page 'Google Search' button visible in the accessibility tree.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:11:40.248Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:13:38.605Z","iterationCount":2,"maxIterations":10,"messages":[{"content":{"text":"User requested clicking the primary 'Google Search' button. Node 394 corresponds to the center page 'Google Search' button visible in the accessibility tree.","type":"reasoning"},"id":"88883efd-8f13-4d4e-ab28-0fa107c13746","timestamp":"2025-10-21T11:12:59.604Z","type":"reasoning"},{"content":{"toolArgs":{"method":"click","nodeId":394,"reasoning":"Click the primary Google Search button to submit the query 'DevTools automation'."},"toolCallId":"3e07f6f2-c12a-4e8b-9ee4-515138c5571d","toolName":"perform_action","type":"tool_call"},"id":"8d4d0214-bc16-455e-86f8-8ecb8f8083eb","timestamp":"2025-10-21T11:13:04.836Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [1100] body"," [1138] heading: Accessibility Links"," [1434] heading: Filters and Topics"," [1440] list"," [1441] listitem"],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":true},"modified":["[1] RootWebArea: Google → [1096] RootWebArea: DevTools automation - Google Search"," [81] div → [1140] div"," [101] navigation → [1435] navigation"," [136] link: About → [1141] link: Skip to main content"," [138] link: Store → [1142] link: Accessibility help"],"removed":[" [8] image: Google"," [536] listbox"," [548] option: devtools automation"," [580] option: chrome devtools automation"," [612] option: axe devtools automation"],"summary":"323 added, 14 removed, 74 modified"},"visualCheck":"- What changed: The page navigated to Google search results for the query \"DevTools automation.\" The search term is visible in the search box, and a full results page (including videos, ads, and related links) is displayed.\n\n- Action success: Yes. The primary Search button click appears to have submitted the query and loaded the results page.\n\n- Errors/unexpected behavior: None detected. No error messages; content looks like a normal Google results page.\n\n- Overall assessment: Successful. The query was submitted and results were displayed as expected.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[3]/center[1]/input[1]"},"success":true,"toolCallId":"3e07f6f2-c12a-4e8b-9ee4-515138c5571d","toolName":"perform_action","type":"tool_result"},"id":"c17a3ce1-23d3-474a-867a-4de4b243806f","timestamp":"2025-10-21T11:13:21.577Z","type":"tool_result"},{"content":{"answer":"Action result summary\n\n- Action performed: Clicked primary Google Search button (node 394) to submit the query \"DevTools automation\".\n- Reasoning followed: As requested to submit via the main search button.\n- Outcome: Success. The pageChange evidence shows changes and the visual check confirms navigation to the Google search results page for \"DevTools automation\". The query is visible in the search input, and a full results page is displayed (videos, links, etc.).\n\nNext steps (choose one or more)\n- Open a specific result: Do you want me to click the first search result link to open it?\n- Extract results data: I can pull a list of top results with titles and URLs. If you want this, I’ll run a data extraction for the results section.\n- Capture a screenshot: I can take a screenshot of the results page (viewport or full page) for visual reference.\n- Refine search: If you want to adjust the query or apply filters (Videos, News, etc.), I can interact with the filters in the UI.","type":"final_answer"},"id":"f19aab82-e2da-4e61-915f-da3e7d0ab619","timestamp":"2025-10-21T11:13:31.668Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"6130b296-3f4b-4971-af1f-677da09589f8","sessionId":"5fe5b519-6308-421b-b798-c977116f75e5","startTime":"2025-10-21T11:12:59.601Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"output":"Action result summary\n\n- Action performed: Clicked primary Google Search button (node 394) to submit the query \"DevTools automation\".\n- Reasoning followed: As requested to submit via the main search button.\n- Outcome: Success. The pageChange evidence shows changes and the visual check confirms navigation to the Google search results page for \"DevTools automation\". The query is visible in the search input, and a full results page is displayed (videos, links, etc.).\n\nNext steps (choose one or more)\n- Open a specific result: Do you want me to click the first search result link to open it?\n- Extract results data: I can pull a list of top results with titles and URLs. If you want this, I’ll run a data extraction for the results section.\n- Capture a screenshot: I can take a screenshot of the results page (viewport or full page) for visual reference.\n- Refine search: If you want to adjust the query or apply filters (Videos, News, etc.), I can interact with the filters in the UI.","success":true,"summary":{"content":"Concise analysis of the conversation\n\n1) User Request\n- Submit a Google search for \"DevTools automation\" by clicking the primary Google Search button (node 394).\n\n2) Agent Decisions\n- Interpreted the instruction as clicking the main search button to submit the query.\n- Executed a click action on node 394 with the stated rationale.\n- Considered a potential fallback (other visible button or pressing Enter) if the suggestions listbox blocked the button, but proceeded with the primary button as planned.\n- Monitored results: noted page changes and verified that the query appeared in the input and the results page loaded.\n\n3) Final Outcome\n- Success: The query was submitted and Google search results for \"DevTools automation\" were displayed.\n- Evidence: pageChange report indicated navigation to the results page; visual check confirmed the query in the input and a full results page.","type":"completion"},"terminationReason":"final_answer"},"success":true,"toolCallId":"50790563-1be1-4625-8ad3-76095dd99f46","toolName":"action_agent","type":"tool_result"},"id":"2054d493-644a-4ee2-8e99-ed8c4e74934e","timestamp":"2025-10-21T11:13:38.605Z","type":"tool_result"},{"content":{"toolArgs":{"duration":3,"reason":"waiting for results","reasoning":"Wait for the search results page to load and render after clicking the 'Google Search' button; check for title/URL change and results content.","seconds":3},"toolCallId":"3522be9c-efbd-4fc1-af33-3f2398057241","toolName":"wait_for_page_load","type":"tool_call"},"id":"0c2a6cba-1114-476b-86a6-c0981ad2b032","timestamp":"2025-10-21T11:13:41.392Z","type":"tool_call"},{"content":{"result":{"completed":true,"reason":"waiting for results","viewportSummary":"No loading spinner is visible; the page appears to be fully loaded. It shows a Google search results view with a \"Search Results\" heading, a \"Videos\" section containing multiple video results, and standard controls (Filters, More filters, Settings, Sign in). Overall, the results are ready and interactive, indicating the wait for results is complete.","waited":3},"success":true,"toolCallId":"3522be9c-efbd-4fc1-af33-3f2398057241","toolName":"wait_for_page_load","type":"tool_result"},"id":"192a594a-aea6-4356-a702-b82e47b125f9","timestamp":"2025-10-21T11:13:50.183Z","type":"tool_result"},{"content":{"toolArgs":{"hint":"Use the page metadata after navigation to report accurate title and URL.","input_data":"","objective":"Retrieve the current page title and URL after navigation to include in the status report.","reasoning":"User requested the resulting page title and URL after performing the search."},"toolCallId":"6812b88a-528a-4ef8-a38b-425fc016e54e","toolName":"action_agent","type":"tool_call"},"id":"d6a96425-62b9-4fe7-9f42-a88ec45243c8","timestamp":"2025-10-21T11:13:52.933Z","type":"tool_call"},{"content":{"context":{"hint":"Use the page metadata after navigation to report accurate title and URL.","input_data":"","objective":"Retrieve the current page title and URL after navigation to include in the status report.","reasoning":"User requested the resulting page title and URL after performing the search."},"nestedSessionId":"ced9dfb1-8ccb-4072-a651-7cb48dbe3fd3","reason":"Handing off to action_agent","targetAgent":"action_agent","type":"handoff"},"id":"5d2029e3-3cb6-4f9c-8adc-7bb0ef4d866a","timestamp":"2025-10-21T11:13:52.934Z","type":"handoff"},{"content":{"result":{"agentSession":{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"User requested the resulting page title and URL after performing the search.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:11:40.248Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:14:40.043Z","iterationCount":3,"maxIterations":10,"messages":[{"content":{"text":"User requested the resulting page title and URL after performing the search.","type":"reasoning"},"id":"1e258b81-a0ad-486c-9b23-7db551419663","timestamp":"2025-10-21T11:13:52.935Z","type":"reasoning"},{"content":{"toolArgs":{"instruction":"Return the current page title and current URL from the page metadata. Provide the title as a string under 'title' and the URL as a string under 'url'.","reasoning":"The user requested the current page title and URL after navigation for inclusion in a status report."},"toolCallId":"08a772f1-101d-4090-9bdc-32f0aa0b8643","toolName":"extract_data","type":"tool_call"},"id":"e5887e35-6744-472d-bf2a-f3b213475c55","timestamp":"2025-10-21T11:14:01.213Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"08a772f1-101d-4090-9bdc-32f0aa0b8643","toolName":"extract_data","type":"tool_result"},"id":"a43bd4fc-d2a0-4387-882e-98a163309c59","timestamp":"2025-10-21T11:14:01.216Z","type":"tool_result"},{"content":{"toolArgs":{"reasoning":"Extract the current page metadata (title and URL) from the visible portion of the page to include in the status report."},"toolCallId":"309f09dd-3ab2-4e2c-a225-27a9e11d549d","toolName":"get_page_content","type":"tool_call"},"id":"7a39b2f1-4f55-4cc3-8cd9-6dcdc497e442","timestamp":"2025-10-21T11:14:06.530Z","type":"tool_call"},{"content":{"result":{"idToUrl":{"1096":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz","1142":"https://support.google.com/websearch/answer/181196?hl=en","1153":"https://www.google.com/webhp?hl=en&sa=X&ved=0ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQPAgI","1413":"https://accounts.google.com/ServiceLogin?hl=en&passive=true&continue=https://www.google.com/search%3Fq%3DDevTools%2Bautomation%26sca_esv%3Dfbaa7d3b9c35fdd6%26source%3Dhp%26ei%3DOGr3aKjoN9yoqtsPpZjHgQg%26iflsig%3DAOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC%26ved%3D0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM%26oq%3DDevTools%2Bautomation%26gs_lp%3DEgdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0%26sclient%3Dgws-wiz&ec=futura_srp_og_si_72236_p","1444":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&udm=50&source=hp&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&aep=1&ntc=1&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ2J8OegQIEBAE","1453":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=7&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&q=DevTools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQtKgLegQIFhAB","1457":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=2&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&q=DevTools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQtKgLegQIFRAB","1461":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=28&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&q=DevTools+automation&ved=1t:220175&ictx=111","1465":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=DevTools+automation&tbm=nws&source=lnms&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ0pQJegQIFBAB","1469":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=39&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&q=DevTools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs6gLegQIExAB","1740":"https://www.youtube.com/watch?v=E1oDoBv68Us","1853":"https://www.youtube.com/watch?v=vC4qH1cCZlg","1964":"https://www.youtube.com/watch?v=Edne8USsZhc","2382":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=7&q=DevTools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8ccDKAR6BAgaEAY","2406":"https://developer.chrome.com/docs/devtools","2445":"https://developer.chrome.com/docs/devtools/tips","2450":"https://developer.chrome.com/docs/devtools/customize","2455":"https://developer.chrome.com/docs/devtools/news","2460":"https://developer.chrome.com/docs/devtools/network/overview","2473":"https://github.com/ChromeDevTools/chrome-devtools-mcp","2513":"https://github.com/ChromeDevTools/chrome-devtools-mcp#:~:text=Security-,Chrome%20DevTools%20MCP","2518":"https://github.com/ChromeDevTools/chrome-devtools-mcp#:~:text=Configuration,-The%20Chrome%20DevTools%20MCP%20server","2523":"https://github.com/ChromeDevTools/chrome-devtools-mcp#:~:text=Concepts","2540":"https://webdriver.io/blog/2019/09/16/devtools/","2588":"https://www.browserstack.com/guide/selenium-devtools","2629":"https://www.browserstack.com/guide/selenium-devtools#toc3","2634":"https://www.browserstack.com/guide/selenium-devtools#toc4","2647":"https://developer.chrome.com/docs/devtools/ai-assistance","2768":"https://www.youtube.com/watch?v=E1oDoBv68Us","2870":"https://www.youtube.com/watch?v=vC4qH1cCZlg","2908":"https://medium.com/@niarsdet/5-groundbreaking-ai-developer-tools-that-are-redefining-software-automation-44502fe06927","2961":"https://jimmysong.io/en/blog/web-automation-advancement/","3002":"https://jimmysong.io/en/blog/web-automation-advancement/#:~:text=Core%20Architecture%20%26%20Components,-Chrome%20DevTools%20MCP%20uses%20a","3007":"https://jimmysong.io/en/blog/web-automation-advancement/#:~:text=Tool%20Ecosystem,-Chrome%20DevTools%20MCP%20provides%2026","3012":"https://jimmysong.io/en/blog/web-automation-advancement/#:~:text=Add%20Chrome%20DevTools%20MCP%20to%20MCP%20Client,-In%20your%20MCP%20client%20%28","3025":"https://addyosmani.com/blog/devtools-mcp/","3068":"https://addyosmani.com/blog/devtools-mcp/#:~:text=This%20directly%20targets,a%20blindfold%20on%E2%80%9D%20because","3071":"https://addyosmani.com/blog/devtools-mcp/#:~:text=1.%20Verify%20code,generating%20a%20solution%2C%20the","3081":"https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation","3122":"https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation#:~:text=DevTools%20Protocol,-Use%20the","3127":"https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation#:~:text=Origin%20trials,-Use%20origin%20trials%20to%20try","3132":"https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation#:~:text=Edge.-,Puppeteer","3183":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Chrome+DevTools&stick=H4sIAAAAAAAAAOMwVOLVT9c3NMwzsMzONjeu-sXIsIGF4RULPxeq-CsWHi4u_Vx9A5Ms8-TKNISCSsOcrGKjLMtXLLxc3CAFhsYmFeVF2Qi-UY5hWUUZ3IAsM3PDnDw4N8ksw9AiD8nCyoKqspKCeIRIskFZbnqFaQpcS3qFkWlK3iJWfueMovzcVAWX1LKQ_Pyc4ltskgy7F3X8ZHHdb7jR0njjJrHH29l_uf-tTZuxchGHWEBqfkFOqkJiTnG-QnFqYlFyhkJaftEKDkYAptL78gUBAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEAo","3195":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Google+Chrome&stick=H4sIAAAAAAAAAOMwVOLSz9U3MMkyT65M-8XIsIGF4RULPxevfrq-oWGegWV2trlx1SsWHi4kZQgFlYY5WcVGWZavWHi5uEEKDI1NKsqLshF8oxzDsooyuAFZZuaGOXlwbpJZhqFFHpKFlQVVZSUF8QiRZIOy3PQK0xS4lvQKI9OUvEWsvO75-ek5qQrOGUX5uam32CQZdi_q-Mniut9wo6Xxxk1ij7ez_3L_W5s2Y-UiDrGA1PwCoNrEnOJ8heLUxKLkDIW0_KIVHIwAVb0s1gABAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEA8","3207":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Playwright&stick=H4sIAAAAAAAAAOMwVOLVT9c3NKw0zMkqNsqy_MXIsIGF4RULPxdEPM_AMjvb3LjqFQsPF5d-rr6BSZZ5cmUaQgFM4ysWXi5ukAJDY5OK8qJsBN8ox7CsogxuQJaZuWFOHpybZJZhaJGHZGFlQVVZSUE8QiTZoCw3vcI0Ba4lvcLINCVvEStXQE5iZXlRZnpGyS02SYbdizp-srjuN9xoabxxk9jj7ey_3P_Wps1YuYhDLCA1vyAnVSExpzhfoTg1sSg5QyEtv2gFByMAsF42iQABAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEBQ","3219":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Visual+Studio+Code&stick=H4sIAAAAAAAAAOMwVOLWz9U3MDQ2qSgvyv7FyLCBheEVCz8Xr366vqFhnoFldra5cdUrFh4uLpA6kyzz5Mo0hIJKw5ysYqMsy1csvFzIBiH4RjmGZRVlcAOyzMwNc_Lg3CSzDEOLPCQLKwuqykoK4hEiyQZluekVpilwLekVRqYpeYtYhcIyi0sTcxSCS0pTMvMVnPNTUm-xSTLsXtTxk8V1v-FGS-ONm8Qeb2f_5f63Nm3GykUcYgGp-QU5qQqJOcX5CsWpiUXJGQpp-UUrOBgB3Q_zdwYBAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEBk","3231":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=React&stick=H4sIAAAAAAAAAOMwVOLWz9U3MDTKMSyrKPvFyLCBheEVCz8Xr366vqFhnoFldra5cdUrFh4uLpA6kyzz5Mo0hIJKw5ysYqMsy1csvFwQg4xNKsqLshF8iMFwA7LMzA1z8uDcJLMMQ4s8JAsrC6rKSgriESLJBmW56RWmKXAt6RVGpil5i1hZg1ITk0tusUky7F7U8ZPFdb_hRkvjjZvEHm9n_-X-tzZtxspFHGIBqfkFOakKiTnF-QrFqYlFyRkKaflFKzgYAaG8vZT5AAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEB4","3243":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Bootstrap&stick=H4sIAAAAAAAAAOMwVOLSz9U3yDIzN8zJ-8XIsIGF4RULPxevfrq-oWGegWV2trlx1SsWHi6wMpMs8-TKNISCSsOcrGKjLMtXLLxc3CAFhsYmFeVF2Qi-UY5hWUUZ3ACIPXBuklmGoUUekoWVBVVlJQXxCJFkg7Lc9ArTFLiW9Aoj05S8RaycTvn5JcUlRYkFt9gkGXYv6vjJ4rrfcKOl8cZNYo-3s_9y_1ubNmPlIg6xgNT8gpxUhcSc4nyF4tTEouQMhbT8ohUcjADUexjN_AAAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkECM","3282":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Devtools+automation+software&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhIEAE","3298":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Devtools+automation+github&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhHEAE","3314":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Chrome+devtools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhKEAE","3330":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Devtools+automation+tutorial&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhGEAE","3347":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Chrome+DevTools&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhJEAE","3363":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=DevTools+download&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhFEAE","3379":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=How+to+open+Developer+Tools+in+Chrome+shortcut&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhLEAE","3395":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Chrome+DevTools+MCP&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhEEAE","3419":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=10&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAE","3422":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=20&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAG","3425":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=30&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAI","3428":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=40&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAK","3431":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=50&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAM","3434":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=60&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAO","3437":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=70&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAQ","3440":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=80&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAS","3443":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=90&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAU","3446":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=10&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8NMDegQIDBAW","3552":"https://support.google.com/websearch/?p=ws_results_help&hl=en&fg=1","3553":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz#","3554":"https://policies.google.com/privacy?hl=en&fg=1","3555":"https://policies.google.com/terms?hl=en&fg=1"},"simplified":"[1096] RootWebArea: DevTools automation - Google Search\n [1100] body\n [1138] heading: Accessibility Links\n [1140] div\n [1141] link: Skip to main content\n [1142] link: Accessibility help\n [1146] link: Accessibility feedback\n [1149] div\n [1118] search\n [1151] div\n [1153] link: Go to Google Home\n [1161] div\n [1172] div\n [1119] combobox: Search\n [3640] StaticText: DevTools automation\n [1190] div\n [1192] button: Clear\n [1197] div\n [1198] button: Search by voice\n [1201] button: Search by image\n [1204] button: Search\n [1394] div\n [1396] button: Settings\n [1404] div\n [1408] button: Google apps\n [1409] image\n [1413] link: Sign in\n [1425] div\n [1433] div\n [1434] heading: Filters and Topics\n [1435] navigation\n [1439] div\n [1440] list\n [1441] listitem\n [1444] link: AI Mode\n [1448] listitem\n [1449] link: All\n [1452] listitem\n [1453] link: Videos\n [1456] listitem\n [1457] link: Images\n [1460] listitem\n [1461] link: Shopping\n [1464] listitem\n [1465] link: News\n [1468] listitem\n [1469] link: Short videos\n [1472] listitem\n [1473] button: More filters\n [3657] StaticText: More\n [1511] button: Tools\n [1661] main\n [1675] div\n [1676] heading: Search Results\n [1677] div\n [1681] div\n [1683] div\n [1684] heading: Videos\n [1687] button: About this result related to Videos\n [1694] div\n [1700] div\n [1701] div\n [1702] button: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful ... by WorldofAI on YouTube. Play on Google. 8:21\n [1734] button: About this result\n [1740] link: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful ... YouTube WorldofAI 6 days ago\n [1741] heading: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful ... YouTube WorldofAI 6 days ago\n [3688] StaticText: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful ...\n [1748] div\n [1749] span\n [3689] StaticText: YouTube\n [3691] StaticText: WorldofAI\n [3692] StaticText: 6 days ago\n [1756] button: Chrome DevTools MCP: AI automation interface for your browser, connects AI agents to Chrome's debugging and automation layer, enables real-time inspection, tracing, and interaction.\n [1813] div\n [1814] div\n [1815] button: Chrome DevTools MCP: Automated Test, Debug and ... by Execute Automation on YouTube. Play on Google. 17:38\n [1847] button: About this result\n [1853] link: Chrome DevTools MCP: Automated Test, Debug and ... YouTube Execute Automation 3 weeks ago\n [1854] heading: Chrome DevTools MCP: Automated Test, Debug and ... YouTube Execute Automation 3 weeks ago\n [3715] StaticText: Chrome DevTools MCP: Automated Test, Debug and ...\n [1861] div\n [1862] span\n [3716] StaticText: YouTube\n [3718] StaticText: Execute Automation\n [3719] StaticText: 3 weeks ago\n [1869] button: Google Chrome's new DevTool MCP server is a powerful AI-powered tool for browser automation and performance analysis, especially useful for developers.\n [1924] div\n [1925] div\n [1926] button: Automating User Flows with Chrome DevTools by Google Developers Space on YouTube. Play on Google. 52:50\n [1958] button: About this result\n [1964] link: Automating User Flows with Chrome DevTools YouTube Google Developers Space Aug 17, 2022\n [1965] heading: Automating User Flows with Chrome DevTools YouTube Google Developers Space Aug 17, 2022\n [3738] StaticText: Automating User Flows with Chrome DevTools\n [1972] div\n [1973] span\n [3739] StaticText: YouTube\n [3741] StaticText: Google Developers Space\n [3742] StaticText: Aug 17, 2022\n [1983] button: 20 key moments in this video\n [2373] group\n [2377] button: Feedback\n [2382] link: View all videos\n [3859] StaticText: View all\n [2401] div\n [2404] div\n [2406] link: DevTools - Chrome for Developers Chrome for Developers https://developer.chrome.com â€ē docs â€ē devtools\n [2407] heading: DevTools - Chrome for Developers\n [2408] LineBreak\n [2414] div\n [3861] StaticText: Chrome for Developers\n [2418] cite\n [3862] StaticText: https://developer.chrome.com\n [3863] StaticText: â€ē docs â€ē devtools\n [2433] button: About this result related to Chrome for Developers\n [2439] div\n [2441] emphasis\n [3867] StaticText: Chrome DevTools\n [3868] StaticText: is a set of web developer tools built directly into the Google Chrome browser. DevTools lets you edit pages on-the-fly and diagnose problems ...\n [2443] list\n [2444] listitem\n [2445] link: DevTools Tips\n [2449] listitem\n [2450] link: Customize DevTools\n [2454] listitem\n [2455] link: What's new in DevTools\n [2459] listitem\n [2460] link: Network panel\n [2468] div\n [2471] div\n [2473] link: ChromeDevTools/chrome-devtools-mcp GitHub https://github.com â€ē ChromeDevTools â€ē chrome-devtool...\n [2474] heading: ChromeDevTools/chrome-devtools-mcp\n [2475] LineBreak\n [2481] div\n [3874] StaticText: GitHub\n [2485] cite\n [3875] StaticText: https://github.com\n [3876] StaticText: â€ē ChromeDevTools â€ē chrome-devtool...\n [2500] button: About this result related to GitHub\n [2506] div\n [3880] StaticText: It acts as a Model-Context-Protocol (MCP) server, giving your AI coding assistant access to the full power of Chrome \n [2508] emphasis\n [3881] StaticText: DevTools\n [3882] StaticText: for reliable \n [2509] emphasis\n [3883] StaticText: automation\n [3884] StaticText: , in-depth ...\n [2511] list\n [2512] listitem\n [2513] link: Chrome Devtools Mcp\n [2517] listitem\n [2518] link: Configuration\n [2522] listitem\n [2523] link: Concepts\n [2535] div\n [2538] div\n [2540] link: Chrome DevTools as Automation Protocol WebdriverIO https://webdriver.io â€ē blog â€ē 2019/09/16 â€ē devtools\n [2541] heading: Chrome DevTools as Automation Protocol\n [2542] LineBreak\n [2548] div\n [3889] StaticText: WebdriverIO\n [2552] cite\n [3890] StaticText: https://webdriver.io\n [3891] StaticText: â€ē blog â€ē 2019/09/16 â€ē devtools\n [2567] button: About this result related to WebdriverIO\n [2573] div\n [3895] StaticText: Sep 16, 2019\n [3897] StaticText: We've created a new NPM package that we call \n [2577] emphasis\n [3898] StaticText: devtools\n [3899] StaticText: which has all WebDriver commands implemented but executes them using Puppeteer.\n [2583] div\n [2586] div\n [2588] link: Selenium DevTools for Advanced Chrome Automation BrowserStack https://www.browserstack.com â€ē Guide\n [2589] heading: Selenium DevTools for Advanced Chrome Automation\n [2590] LineBreak\n [2596] div\n [3901] StaticText: BrowserStack\n [2600] cite\n [3902] StaticText: https://www.browserstack.com\n [3903] StaticText: â€ē Guide\n [2615] button: About this result related to BrowserStack\n [2621] div\n [3907] StaticText: Jan 30, 2025\n [2625] emphasis\n [3909] StaticText: Selenium DevTools\n [3910] StaticText: is an interface that integrates Chrome Debugging Protocol (CDP) with Selenium, enabling deeper browser control, debugging, and network ...\n [2627] list\n [2628] listitem\n [2629] link: Key Features of Selenium...\n [2633] listitem\n [2634] link: Setting Up Selenium Dev Tools\n [2642] div\n [2645] div\n [2647] link: DevTools Get started AI assistance - Chrome for Developers Chrome for Developers https://developer.chrome.com â€ē ... â€ē Chrome DevTools\n [2648] heading: DevTools Get started AI assistance - Chrome for Developers\n [2649] LineBreak\n [2655] div\n [3914] StaticText: Chrome for Developers\n [2659] cite\n [3915] StaticText: https://developer.chrome.com\n [3916] StaticText: â€ē ... â€ē Chrome DevTools\n [2674] button: About this result related to Chrome for Developers\n [2680] div\n [3920] StaticText: This interactive guide helps you to \n [2682] emphasis\n [3921] StaticText: enable and test AI assistance for styling in Chrome DevTools\n [3922] StaticText: in just three quick steps. Open Chrome DevTools.\n [2688] g-section-with-header\n [2691] heading: What people are saying\n [2697] list\n [2698] list\n [2699] listitem\n [2703] div\n [2708] button: Video. Play on Google. 8:21\n [2768] link: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful Tool For Coding Agents! (Fully Free) 18.5K+ views ¡ 6 days ago WorldofAI ¡ YouTube AI applications tips, tricks & guides\n [2769] div\n [2771] heading: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful Tool For Coding Agents! (Fully Free)\n [2773] div\n [3937] StaticText: 18.5K+ views\n [3939] StaticText: 6 days ago\n [2789] div\n [3940] StaticText: WorldofAI ¡ YouTube\n [3941] StaticText: AI applications tips, tricks & guides\n [2796] button: About this result\n [2801] listitem\n [2805] div\n [2810] button: Video. Play on Google. 17:38\n [2870] link: Chrome DevTools MCP: Automated Test, Debug and Performance Analysis with AI 6.8K+ views ¡ 3 weeks ago Execute Automation ¡ YouTube Automation testing training\n [2871] div\n [2873] heading: Chrome DevTools MCP: Automated Test, Debug and Performance Analysis with AI\n [2875] div\n [3955] StaticText: 6.8K+ views\n [3957] StaticText: 3 weeks ago\n [2891] div\n [3958] StaticText: Execute Automation ¡ YouTube\n [3959] StaticText: Automation testing training\n [2898] button: About this result\n [2903] listitem\n [2907] div\n [2908] link: 5 Groundbreaking AI Developer Tools That Are Redefining Software Automation 2 likes ¡ Jul 26, 2025 Niar Medium\n [2913] div\n [2915] heading: 5 Groundbreaking AI Developer Tools That Are Redefining Software Automation\n [2916] div\n [3961] StaticText: 2 likes\n [3963] StaticText: Jul 26, 2025\n [2928] div\n [3964] StaticText: Niar\n [3965] StaticText: Medium\n [2934] button: About this result\n [2956] div\n [2959] div\n [2961] link: Chrome DevTools MCP: A New Era of Frontend Automation jimmysong.io https://jimmysong.io â€ē Jimmy Song's Blog â€ē Blog\n [2962] heading: Chrome DevTools MCP: A New Era of Frontend Automation\n [2963] LineBreak\n [2969] div\n [3967] StaticText: jimmysong.io\n [2973] cite\n [3968] StaticText: https://jimmysong.io\n [3969] StaticText: â€ē Jimmy Song's Blog â€ē Blog\n [2988] button: About this result\n [2994] div\n [3973] StaticText: Sep 25, 2025\n [2998] emphasis\n [3975] StaticText: Chrome DevTools MCP\n [3976] StaticText: is not just an API for DevTools features—it wraps debugging, performance tracing, network monitoring, and more as an MCP ...\n [3000] list\n [3001] listitem\n [3002] link: Core Architecture &...\n [3006] listitem\n [3007] link: Tool Ecosystem\n [3011] listitem\n [3012] link: Add Chrome Devtools Mcp To...\n [3020] div\n [3023] div\n [3025] link: Give your AI eyes: Introducing Chrome DevTools MCP Addy Osmani https://addyosmani.com â€ē blog â€ē devtools-mcp\n [3026] heading: Give your AI eyes: Introducing Chrome DevTools MCP\n [3027] LineBreak\n [3033] div\n [3981] StaticText: Addy Osmani\n [3037] cite\n [3982] StaticText: https://addyosmani.com\n [3983] StaticText: â€ē blog â€ē devtools-mcp\n [3052] button: About this result related to Addy Osmani\n [3058] div\n [3987] StaticText: Sep 25, 2025\n [3062] emphasis\n [3989] StaticText: Chrome DevTools MCP\n [3990] StaticText: is essentially an MCP server that exposes Chrome's debugging and automation surface to AI assistants. It acts as a bridge ...\n [3064] div\n [3991] StaticText: Page includes:\n [3068] link: Limitations\n [3071] link: Verification\n [3076] div\n [3079] div\n [3081] link: Test and automation in Microsoft Edge Microsoft Learn https://learn.microsoft.com â€ē en-us â€ē test-and-automation\n [3082] heading: Test and automation in Microsoft Edge\n [3083] LineBreak\n [3089] div\n [3997] StaticText: Microsoft Learn\n [3093] cite\n [3998] StaticText: https://learn.microsoft.com\n [3999] StaticText: â€ē en-us â€ē test-and-automation\n [3108] button: About this result related to Microsoft Learn\n [3114] div\n [4003] StaticText: Oct 31, 2024\n [3118] emphasis\n [4005] StaticText: Use the DevTools Protocol\n [4006] StaticText: to instrument, inspect, debug, and profile browsers, including Microsoft Edge.\n [3120] list\n [3121] listitem\n [3122] link: Devtools Protocol\n [3126] listitem\n [3127] link: Origin Trials\n [3131] listitem\n [3132] link: Puppeteer\n [3139] region: Ads\n [3140] heading: Ads\n [3154] div\n [3166] div\n [3168] heading: People also search for\n [3178] list\n [3179] listitem\n [3183] link: Chrome DevTools\n [3191] listitem\n [3195] link: Google Chrome\n [3203] listitem\n [3207] link: Playwright\n [3215] listitem\n [3219] link: Visual Studio Code\n [3227] listitem\n [3231] link: React\n [3239] listitem\n [3243] link: Bootstrap\n [3268] group\n [3272] button: Feedback\n [3277] div\n [3278] div\n [3282] link: Devtools automation software\n [3288] span\n [4019] StaticText: Devtools automation \n [4020] StaticText: software\n [3298] link: Devtools automation github\n [3304] span\n [4021] StaticText: Devtools automation \n [4022] StaticText: github\n [3314] link: Chrome devtools automation\n [3320] span\n [4023] StaticText: Chrome\n [4024] StaticText: devtools automation\n [3330] link: Devtools automation tutorial\n [3336] span\n [4025] StaticText: Devtools automation \n [4026] StaticText: tutorial\n [3343] div\n [3347] link: Chrome DevTools\n [3353] span\n [4027] StaticText: Chrome\n [4028] StaticText: DevTools\n [3363] link: DevTools download\n [3369] span\n [4029] StaticText: DevTools \n [4030] StaticText: download\n [3379] link: How to open Developer Tools in Chrome shortcut\n [3395] link: Chrome DevTools MCP\n [3401] span\n [4032] StaticText: Chrome\n [4033] StaticText: DevTools \n [4034] StaticText: MCP\n [3409] navigation\n [3410] heading: Page Navigation\n [3413] tr\n [3419] link: Page 2\n [3422] link: Page 3\n [3425] link: Page 4\n [3428] link: Page 5\n [3431] link: Page 6\n [3434] link: Page 7\n [3437] link: Page 8\n [3440] link: Page 9\n [3443] link: Page 10\n [4045] StaticText: 10\n [3445] heading: Next\n [3446] link: Next\n [3451] navigation\n [3494] contentinfo\n [3495] heading: Footer Links\n [3499] div\n [3504] div\n [3506] button: Mueller, Austin, TX  - Based on your past activity\n [4048] StaticText: Mueller, Austin, TX\n [4050] StaticText: Based on your past activity\n [3526] button: Update location\n [3551] span\n [3552] link: Help\n [3553] link: Send feedback\n [3554] link: Privacy\n [3555] link: Terms\n"},"success":true,"toolCallId":"309f09dd-3ab2-4e2c-a225-27a9e11d549d","toolName":"get_page_content","type":"tool_result"},"id":"4a9563ae-897e-4c62-b9f3-b5e765498841","timestamp":"2025-10-21T11:14:06.596Z","type":"tool_result"},{"content":{"answer":"Here are the current page details:\n\n- Title: DevTools automation - Google Search\n- URL: https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz","type":"final_answer"},"id":"73142fbb-e521-4e01-9a90-38bec7fad928","timestamp":"2025-10-21T11:14:21.213Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"6130b296-3f4b-4971-af1f-677da09589f8","sessionId":"ced9dfb1-8ccb-4072-a651-7cb48dbe3fd3","startTime":"2025-10-21T11:13:52.934Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"output":"Here are the current page details:\n\n- Title: DevTools automation - Google Search\n- URL: https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz","success":true,"summary":{"content":"Concise analysis of the conversation\n\n1) User Request\n- Asked to retrieve the current page title and URL after navigation, in order to include these in a status report. Provided input data: .\n\n2) Agent Decisions and Actions\n- Initial approach: attempted data extraction via a structured tool call (functions.extract_data) to return title and url in a JSON-like structure.\n- Challenge: the tool responded that a JSON Schema was required, causing a temporary halt and a schema prompt.\n- Adaptation: switched to directly reading the page metadata via a page content retrieval step (functions.get_page_content).\n- Progress: parsed the page content and identified the current page as a Google Search results page with the title “DevTools automation - Google Search.”\n- Data mapping: located the current page URL in the content’s idToUrl mapping (id 1096 corresponds to the Google search results URL for the query “DevTools automation”).\n- Outcome: produced the requested outputs (title and URL) based on the retrieved metadata.\n\n3) Final Outcome\n- Title retrieved: “DevTools automation - Google Search”\n- URL retrieved: the Google search results URL for the query “DevTools automation” (long URL with various tracking parameters as shown in the data):\n https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz\n\nNotes for next steps (actionable insights)\n- If continuing automation across pages, ensure the agent consistently captures the final navigated page (document.title and window.location.href or equivalent) after all navigation and potential dynamic content loads.\n- Consider returning data in a strict, machine-readable schema (e.g., a simple { \"title\": \"...\", \"url\": \"...\" } JSON) to avoid late-schema prompts or parsing ambiguities.\n- For dynamic pages, include a wait-for-load step or a check that the page is fully loaded before extracting title/URL.\n- In multi-agent workflows, standardize a minimal contract: after navigation, report exactly the title and final URL, plus a brief status flag (e.g., \"loaded\": true).","type":"completion"},"terminationReason":"final_answer"},"success":true,"toolCallId":"6812b88a-528a-4ef8-a38b-425fc016e54e","toolName":"action_agent","type":"tool_result"},"id":"0f4b8c7d-7dec-49de-bca4-1cf4796f1f3d","timestamp":"2025-10-21T11:14:40.044Z","type":"tool_result"},{"content":{"answer":"Status: Success — search submitted and results page loaded.\n\nPage title: DevTools automation - Google Search\n\nPage URL: https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz\n\nNotes:\n- I focused the main search combobox, entered \"DevTools automation\", and clicked the primary \"Google Search\" button. A suggestions list briefly appeared but did not prevent submission.\n- No sign-in prompts, dialogs, or CAPTCHAs were encountered.\n- If you want, I can (a) take a screenshot of the results page, (b) open the first result, or (c) extract the top result titles/URLs. Which would you like next?","type":"final_answer"},"id":"b65c4cbe-2749-463b-9210-498b24609cc2","timestamp":"2025-10-21T11:14:52.351Z","type":"final_answer"}],"modelUsed":"gpt-5-mini","nestedSessions":[{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"The accessibility tree shows node [2] is a combobox labeled 'Search' — this is the main Google search input. Focusing it first ensures typing goes to the correct element and avoids interference from other UI elements like AI Mode or voice search.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:11:40.248Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:12:36.631Z","iterationCount":3,"maxIterations":10,"messages":[{"content":{"text":"The accessibility tree shows node [2] is a combobox labeled 'Search' — this is the main Google search input. Focusing it first ensures typing goes to the correct element and avoids interference from other UI elements like AI Mode or voice search.","type":"reasoning"},"id":"2a14e0be-0724-4dd3-bd46-c0433a7307b0","timestamp":"2025-10-21T11:11:40.251Z","type":"reasoning"},{"content":{"toolArgs":{"method":"click","nodeId":2,"reasoning":"Focus the main search combobox labeled 'Search' by clicking on the combobox element to ensure it receives keyboard input."},"toolCallId":"a9f80474-1f3e-48e8-a6bf-cb5e35b324c3","toolName":"perform_action","type":"tool_call"},"id":"e9c53be2-d616-4842-80b6-d680d786d47d","timestamp":"2025-10-21T11:11:47.296Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [253] div"," [269] div"," [272] div"," [859] StaticText: Related to recent searches"," [536] listbox"],"hasChanges":true,"hasMore":{"added":true,"modified":false,"removed":false},"modified":[],"removed":[],"summary":"19 added, 0 removed, 0 modified"},"visualCheck":"- What changed in the page content\n - The page shows the Google search area with a combobox labeled “Search” present in the DOM.\n - A listbox with related search options is visible, along with the standard Google Search and I’m Feeling Lucky buttons.\n - The specific target in the action (a textarea inside the combobox) is not visible in the provided page structure.\n\n- Did the action appear to succeed?\n - It’s not clearly verifiable from the static content. There is no explicit indication that focus moved to the combobox. The combobox element exists, but there’s no visible focus state or input change shown.\n\n- Any error messages or unexpected behavior\n - No errors or unexpected behavior are shown in the content.\n\n- Overall assessment\n - Unclear whether the click succeeded in focusing the combobox. The DOM element referenced by the action (a textarea inside the combobox) does not appear in the provided view, suggesting a mismatch between the target and the actual element, or that focus state is not reflected in this static dump. To verify, check whether the active element is the combobox after the click or try typing into the search field to confirm input capture. If possible, adjust the selector to the actual input/combobox element and re-run the action.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"a9f80474-1f3e-48e8-a6bf-cb5e35b324c3","toolName":"perform_action","type":"tool_result"},"id":"1fcf77c0-23ab-4842-9d01-b49765b88b19","timestamp":"2025-10-21T11:12:01.519Z","type":"tool_result"},{"content":{"toolArgs":{"args":{"text":"DevTools automation"},"method":"fill","nodeId":2,"reasoning":"Type the phrase into the main search combobox (node 2) to set the query for Google search."},"toolCallId":"623c725c-9d5a-461f-a21d-717d5ba67900","toolName":"perform_action","type":"tool_call"},"id":"fdcae125-d403-480e-a2d9-1441aeb0ed99","timestamp":"2025-10-21T11:12:09.570Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [222] button: Clear"," [536] listbox"," [1049] span"," [1070] StaticText: chrome "," [1071] StaticText: devtools automation"],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":false},"modified":[" [859] StaticText: Related to recent searches → [931] StaticText: DevTools automation"," [548] option: chrome devtools recorder → [580] option: chrome devtools automation"," [580] option: chrome devtools → [548] option: devtools automation"," [612] option: @wdio/devtools-service → [644] option: what are the automation tools in devops"," [644] option: ⤕āĨā¤°āĨ‹ā¤Ž → [612] option: axe devtools automation"],"removed":[" [272] div"," [536] listbox"," [804] option: chrome beta"],"summary":"30 added, 3 removed, 10 modified"},"visualCheck":"- Changes observed:\n - The main search combobox (node 2) now shows the input value \"DevTools automation\" (StaticText under the combobox reflects the filled text).\n\n- Action success:\n - The fill action appears to have succeeded. The page content shows the combobox containing the expected value and the verification status states the fill action was verified successfully.\n\n- Errors or unexpected behavior:\n - No errors or warnings are present. The available options beneath the combobox include various variants (e.g., \"devtools automation\", \"chrome devtools automation\"), which is typical for a suggestions list.\n\n- Overall assessment:\n - The action succeeded: the phrase was entered into the main search combobox as intended, and the UI reflects the updated value. Ready to proceed with the search if desired.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"623c725c-9d5a-461f-a21d-717d5ba67900","toolName":"perform_action","type":"tool_result"},"id":"0a7fd2d0-ec08-4cdf-a69c-ce4436a0d646","timestamp":"2025-10-21T11:12:19.908Z","type":"tool_result"},{"content":{"answer":"Action completed.\n\n- What I did:\n - Focused the main search combobox (nodeId 2) and typed the phrase: DevTools automation.\n\n- Result:\n - The input now shows the value \"DevTools automation\" in the combobox. The pageChange evidence indicates the fill action succeeded and the UI updated accordingly.\n - There are related-suggestions beneath the combobox (e.g., devtools automation, chrome devtools automation).\n\n- Next steps (choose one):\n - Submit the search by pressing Enter in the combobox:\n - perform_action: { method: \"press\", nodeId: 2, \"args\": [\"Enter\"] }\n - Or click the Google Search button to execute the search:\n - perform_action: { method: \"click\", nodeId: 382 } (the visible \"Google Search\" button in the main area)\n\nWould you like me to submit the search now or keep refining the input first?","type":"final_answer"},"id":"f47ef0d2-5aeb-41ee-8817-349770ae52d0","timestamp":"2025-10-21T11:12:28.855Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"6130b296-3f4b-4971-af1f-677da09589f8","sessionId":"a1fbaf10-3747-4c12-9d83-2ed2ea3a9398","startTime":"2025-10-21T11:11:40.249Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"User requested clicking the primary 'Google Search' button. Node 394 corresponds to the center page 'Google Search' button visible in the accessibility tree.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:11:40.248Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:13:38.605Z","iterationCount":2,"maxIterations":10,"messages":[{"content":{"text":"User requested clicking the primary 'Google Search' button. Node 394 corresponds to the center page 'Google Search' button visible in the accessibility tree.","type":"reasoning"},"id":"88883efd-8f13-4d4e-ab28-0fa107c13746","timestamp":"2025-10-21T11:12:59.604Z","type":"reasoning"},{"content":{"toolArgs":{"method":"click","nodeId":394,"reasoning":"Click the primary Google Search button to submit the query 'DevTools automation'."},"toolCallId":"3e07f6f2-c12a-4e8b-9ee4-515138c5571d","toolName":"perform_action","type":"tool_call"},"id":"8d4d0214-bc16-455e-86f8-8ecb8f8083eb","timestamp":"2025-10-21T11:13:04.836Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [1100] body"," [1138] heading: Accessibility Links"," [1434] heading: Filters and Topics"," [1440] list"," [1441] listitem"],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":true},"modified":["[1] RootWebArea: Google → [1096] RootWebArea: DevTools automation - Google Search"," [81] div → [1140] div"," [101] navigation → [1435] navigation"," [136] link: About → [1141] link: Skip to main content"," [138] link: Store → [1142] link: Accessibility help"],"removed":[" [8] image: Google"," [536] listbox"," [548] option: devtools automation"," [580] option: chrome devtools automation"," [612] option: axe devtools automation"],"summary":"323 added, 14 removed, 74 modified"},"visualCheck":"- What changed: The page navigated to Google search results for the query \"DevTools automation.\" The search term is visible in the search box, and a full results page (including videos, ads, and related links) is displayed.\n\n- Action success: Yes. The primary Search button click appears to have submitted the query and loaded the results page.\n\n- Errors/unexpected behavior: None detected. No error messages; content looks like a normal Google results page.\n\n- Overall assessment: Successful. The query was submitted and results were displayed as expected.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[3]/center[1]/input[1]"},"success":true,"toolCallId":"3e07f6f2-c12a-4e8b-9ee4-515138c5571d","toolName":"perform_action","type":"tool_result"},"id":"c17a3ce1-23d3-474a-867a-4de4b243806f","timestamp":"2025-10-21T11:13:21.577Z","type":"tool_result"},{"content":{"answer":"Action result summary\n\n- Action performed: Clicked primary Google Search button (node 394) to submit the query \"DevTools automation\".\n- Reasoning followed: As requested to submit via the main search button.\n- Outcome: Success. The pageChange evidence shows changes and the visual check confirms navigation to the Google search results page for \"DevTools automation\". The query is visible in the search input, and a full results page is displayed (videos, links, etc.).\n\nNext steps (choose one or more)\n- Open a specific result: Do you want me to click the first search result link to open it?\n- Extract results data: I can pull a list of top results with titles and URLs. If you want this, I’ll run a data extraction for the results section.\n- Capture a screenshot: I can take a screenshot of the results page (viewport or full page) for visual reference.\n- Refine search: If you want to adjust the query or apply filters (Videos, News, etc.), I can interact with the filters in the UI.","type":"final_answer"},"id":"f19aab82-e2da-4e61-915f-da3e7d0ab619","timestamp":"2025-10-21T11:13:31.668Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"6130b296-3f4b-4971-af1f-677da09589f8","sessionId":"5fe5b519-6308-421b-b798-c977116f75e5","startTime":"2025-10-21T11:12:59.601Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"User requested the resulting page title and URL after performing the search.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:11:40.248Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:14:40.043Z","iterationCount":3,"maxIterations":10,"messages":[{"content":{"text":"User requested the resulting page title and URL after performing the search.","type":"reasoning"},"id":"1e258b81-a0ad-486c-9b23-7db551419663","timestamp":"2025-10-21T11:13:52.935Z","type":"reasoning"},{"content":{"toolArgs":{"instruction":"Return the current page title and current URL from the page metadata. Provide the title as a string under 'title' and the URL as a string under 'url'.","reasoning":"The user requested the current page title and URL after navigation for inclusion in a status report."},"toolCallId":"08a772f1-101d-4090-9bdc-32f0aa0b8643","toolName":"extract_data","type":"tool_call"},"id":"e5887e35-6744-472d-bf2a-f3b213475c55","timestamp":"2025-10-21T11:14:01.213Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"08a772f1-101d-4090-9bdc-32f0aa0b8643","toolName":"extract_data","type":"tool_result"},"id":"a43bd4fc-d2a0-4387-882e-98a163309c59","timestamp":"2025-10-21T11:14:01.216Z","type":"tool_result"},{"content":{"toolArgs":{"reasoning":"Extract the current page metadata (title and URL) from the visible portion of the page to include in the status report."},"toolCallId":"309f09dd-3ab2-4e2c-a225-27a9e11d549d","toolName":"get_page_content","type":"tool_call"},"id":"7a39b2f1-4f55-4cc3-8cd9-6dcdc497e442","timestamp":"2025-10-21T11:14:06.530Z","type":"tool_call"},{"content":{"result":{"idToUrl":{"1096":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz","1142":"https://support.google.com/websearch/answer/181196?hl=en","1153":"https://www.google.com/webhp?hl=en&sa=X&ved=0ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQPAgI","1413":"https://accounts.google.com/ServiceLogin?hl=en&passive=true&continue=https://www.google.com/search%3Fq%3DDevTools%2Bautomation%26sca_esv%3Dfbaa7d3b9c35fdd6%26source%3Dhp%26ei%3DOGr3aKjoN9yoqtsPpZjHgQg%26iflsig%3DAOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC%26ved%3D0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM%26oq%3DDevTools%2Bautomation%26gs_lp%3DEgdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0%26sclient%3Dgws-wiz&ec=futura_srp_og_si_72236_p","1444":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&udm=50&source=hp&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&aep=1&ntc=1&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ2J8OegQIEBAE","1453":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=7&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&q=DevTools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQtKgLegQIFhAB","1457":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=2&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&q=DevTools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQtKgLegQIFRAB","1461":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=28&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&q=DevTools+automation&ved=1t:220175&ictx=111","1465":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=DevTools+automation&tbm=nws&source=lnms&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ0pQJegQIFBAB","1469":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=39&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZud1z6kQpMfoEdCJxnpm_3WBcADgXS0kQ7L92bqYkFCFNOV_HVSgOLkzTtMTzzOoU9E7al0ivYItrO6EoUe-HW1iOl73fkm5mHKkC4QbpJ_9OfQFUG6x9jp3K3j00kLH4U4fAaCPdN4b_O0ecrL_JJt72j6Ypnlg5y7aLYMDLgzO7oNUrB2rJ4rajig55FxOmbgYTnw&q=DevTools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs6gLegQIExAB","1740":"https://www.youtube.com/watch?v=E1oDoBv68Us","1853":"https://www.youtube.com/watch?v=vC4qH1cCZlg","1964":"https://www.youtube.com/watch?v=Edne8USsZhc","2382":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&udm=7&q=DevTools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8ccDKAR6BAgaEAY","2406":"https://developer.chrome.com/docs/devtools","2445":"https://developer.chrome.com/docs/devtools/tips","2450":"https://developer.chrome.com/docs/devtools/customize","2455":"https://developer.chrome.com/docs/devtools/news","2460":"https://developer.chrome.com/docs/devtools/network/overview","2473":"https://github.com/ChromeDevTools/chrome-devtools-mcp","2513":"https://github.com/ChromeDevTools/chrome-devtools-mcp#:~:text=Security-,Chrome%20DevTools%20MCP","2518":"https://github.com/ChromeDevTools/chrome-devtools-mcp#:~:text=Configuration,-The%20Chrome%20DevTools%20MCP%20server","2523":"https://github.com/ChromeDevTools/chrome-devtools-mcp#:~:text=Concepts","2540":"https://webdriver.io/blog/2019/09/16/devtools/","2588":"https://www.browserstack.com/guide/selenium-devtools","2629":"https://www.browserstack.com/guide/selenium-devtools#toc3","2634":"https://www.browserstack.com/guide/selenium-devtools#toc4","2647":"https://developer.chrome.com/docs/devtools/ai-assistance","2768":"https://www.youtube.com/watch?v=E1oDoBv68Us","2870":"https://www.youtube.com/watch?v=vC4qH1cCZlg","2908":"https://medium.com/@niarsdet/5-groundbreaking-ai-developer-tools-that-are-redefining-software-automation-44502fe06927","2961":"https://jimmysong.io/en/blog/web-automation-advancement/","3002":"https://jimmysong.io/en/blog/web-automation-advancement/#:~:text=Core%20Architecture%20%26%20Components,-Chrome%20DevTools%20MCP%20uses%20a","3007":"https://jimmysong.io/en/blog/web-automation-advancement/#:~:text=Tool%20Ecosystem,-Chrome%20DevTools%20MCP%20provides%2026","3012":"https://jimmysong.io/en/blog/web-automation-advancement/#:~:text=Add%20Chrome%20DevTools%20MCP%20to%20MCP%20Client,-In%20your%20MCP%20client%20%28","3025":"https://addyosmani.com/blog/devtools-mcp/","3068":"https://addyosmani.com/blog/devtools-mcp/#:~:text=This%20directly%20targets,a%20blindfold%20on%E2%80%9D%20because","3071":"https://addyosmani.com/blog/devtools-mcp/#:~:text=1.%20Verify%20code,generating%20a%20solution%2C%20the","3081":"https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation","3122":"https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation#:~:text=DevTools%20Protocol,-Use%20the","3127":"https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation#:~:text=Origin%20trials,-Use%20origin%20trials%20to%20try","3132":"https://learn.microsoft.com/en-us/microsoft-edge/test-and-automation/test-and-automation#:~:text=Edge.-,Puppeteer","3183":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Chrome+DevTools&stick=H4sIAAAAAAAAAOMwVOLVT9c3NMwzsMzONjeu-sXIsIGF4RULPxeq-CsWHi4u_Vx9A5Ms8-TKNISCSsOcrGKjLMtXLLxc3CAFhsYmFeVF2Qi-UY5hWUUZ3IAsM3PDnDw4N8ksw9AiD8nCyoKqspKCeIRIskFZbnqFaQpcS3qFkWlK3iJWfueMovzcVAWX1LKQ_Pyc4ltskgy7F3X8ZHHdb7jR0njjJrHH29l_uf-tTZuxchGHWEBqfkFOqkJiTnG-QnFqYlFyhkJaftEKDkYAptL78gUBAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEAo","3195":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Google+Chrome&stick=H4sIAAAAAAAAAOMwVOLSz9U3MMkyT65M-8XIsIGF4RULPxevfrq-oWGegWV2trlx1SsWHi4kZQgFlYY5WcVGWZavWHi5uEEKDI1NKsqLshF8oxzDsooyuAFZZuaGOXlwbpJZhqFFHpKFlQVVZSUF8QiRZIOy3PQK0xS4lvQKI9OUvEWsvO75-ek5qQrOGUX5uam32CQZdi_q-Mniut9wo6Xxxk1ij7ez_3L_W5s2Y-UiDrGA1PwCoNrEnOJ8heLUxKLkDIW0_KIVHIwAVb0s1gABAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEA8","3207":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Playwright&stick=H4sIAAAAAAAAAOMwVOLVT9c3NKw0zMkqNsqy_MXIsIGF4RULPxdEPM_AMjvb3LjqFQsPF5d-rr6BSZZ5cmUaQgFM4ysWXi5ukAJDY5OK8qJsBN8ox7CsogxuQJaZuWFOHpybZJZhaJGHZGFlQVVZSUE8QiTZoCw3vcI0Ba4lvcLINCVvEStXQE5iZXlRZnpGyS02SYbdizp-srjuN9xoabxxk9jj7ey_3P_Wps1YuYhDLCA1vyAnVSExpzhfoTg1sSg5QyEtv2gFByMAsF42iQABAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEBQ","3219":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Visual+Studio+Code&stick=H4sIAAAAAAAAAOMwVOLWz9U3MDQ2qSgvyv7FyLCBheEVCz8Xr366vqFhnoFldra5cdUrFh4uLpA6kyzz5Mo0hIJKw5ysYqMsy1csvFzIBiH4RjmGZRVlcAOyzMwNc_Lg3CSzDEOLPCQLKwuqykoK4hEiyQZluekVpilwLekVRqYpeYtYhcIyi0sTcxSCS0pTMvMVnPNTUm-xSTLsXtTxk8V1v-FGS-ONm8Qeb2f_5f63Nm3GykUcYgGp-QU5qQqJOcX5CsWpiUXJGQpp-UUrOBgB3Q_zdwYBAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEBk","3231":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=React&stick=H4sIAAAAAAAAAOMwVOLWz9U3MDTKMSyrKPvFyLCBheEVCz8Xr366vqFhnoFldra5cdUrFh4uLpA6kyzz5Mo0hIJKw5ysYqMsy1csvFwQg4xNKsqLshF8iMFwA7LMzA1z8uDcJLMMQ4s8JAsrC6rKSgriESLJBmW56RWmKXAt6RVGpil5i1hZg1ITk0tusUky7F7U8ZPFdb_hRkvjjZvEHm9n_-X-tzZtxspFHGIBqfkFOakKiTnF-QrFqYlFyRkKaflFKzgYAaG8vZT5AAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkEB4","3243":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Bootstrap&stick=H4sIAAAAAAAAAOMwVOLSz9U3yDIzN8zJ-8XIsIGF4RULPxevfrq-oWGegWV2trlx1SsWHi6wMpMs8-TKNISCSsOcrGKjLMtXLLxc3CAFhsYmFeVF2Qi-UY5hWUUZ3ACIPXBuklmGoUUekoWVBVVlJQXxCJFkg7Lc9ArTFLiW9Aoj05S8RaycTvn5JcUlRYkFt9gkGXYv6vjJ4rrfcKOl8cZNYo-3s_9y_1ubNmPlIg6xgNT8gpxUhcSc4nyF4tTEouQMhbT8ohUcjADUexjN_AAAAA&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQs9oBKAB6BAgkECM","3282":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Devtools+automation+software&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhIEAE","3298":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Devtools+automation+github&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhHEAE","3314":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Chrome+devtools+automation&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhKEAE","3330":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Devtools+automation+tutorial&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhGEAE","3347":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Chrome+DevTools&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhJEAE","3363":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=DevTools+download&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhFEAE","3379":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=How+to+open+Developer+Tools+in+Chrome+shortcut&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhLEAE","3395":"https://www.google.com/search?sca_esv=fbaa7d3b9c35fdd6&q=Chrome+DevTools+MCP&sa=X&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ1QJ6BAhEEAE","3419":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=10&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAE","3422":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=20&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAG","3425":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=30&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAI","3428":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=40&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAK","3431":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=50&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAM","3434":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=60&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAO","3437":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=70&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAQ","3440":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=80&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAS","3443":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=90&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8tMDegQIDBAU","3446":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&ei=w2r3aIz8Av-sqtsP0ZOjkAM&start=10&sa=N&sstk=Af77f_czishgVmnh7cKAyhdc1BlLtbUGa8RZ9JLuFR2NgbXaJABnSwoGeiY9X_EYZMJoaZ9yxbnshEce5Iy2h03LWr_cius7kA6lxw&ved=2ahUKEwjM6brGlLWQAxV_lmoFHdHJCDIQ8NMDegQIDBAW","3552":"https://support.google.com/websearch/?p=ws_results_help&hl=en&fg=1","3553":"https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz#","3554":"https://policies.google.com/privacy?hl=en&fg=1","3555":"https://policies.google.com/terms?hl=en&fg=1"},"simplified":"[1096] RootWebArea: DevTools automation - Google Search\n [1100] body\n [1138] heading: Accessibility Links\n [1140] div\n [1141] link: Skip to main content\n [1142] link: Accessibility help\n [1146] link: Accessibility feedback\n [1149] div\n [1118] search\n [1151] div\n [1153] link: Go to Google Home\n [1161] div\n [1172] div\n [1119] combobox: Search\n [3640] StaticText: DevTools automation\n [1190] div\n [1192] button: Clear\n [1197] div\n [1198] button: Search by voice\n [1201] button: Search by image\n [1204] button: Search\n [1394] div\n [1396] button: Settings\n [1404] div\n [1408] button: Google apps\n [1409] image\n [1413] link: Sign in\n [1425] div\n [1433] div\n [1434] heading: Filters and Topics\n [1435] navigation\n [1439] div\n [1440] list\n [1441] listitem\n [1444] link: AI Mode\n [1448] listitem\n [1449] link: All\n [1452] listitem\n [1453] link: Videos\n [1456] listitem\n [1457] link: Images\n [1460] listitem\n [1461] link: Shopping\n [1464] listitem\n [1465] link: News\n [1468] listitem\n [1469] link: Short videos\n [1472] listitem\n [1473] button: More filters\n [3657] StaticText: More\n [1511] button: Tools\n [1661] main\n [1675] div\n [1676] heading: Search Results\n [1677] div\n [1681] div\n [1683] div\n [1684] heading: Videos\n [1687] button: About this result related to Videos\n [1694] div\n [1700] div\n [1701] div\n [1702] button: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful ... by WorldofAI on YouTube. Play on Google. 8:21\n [1734] button: About this result\n [1740] link: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful ... YouTube WorldofAI 6 days ago\n [1741] heading: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful ... YouTube WorldofAI 6 days ago\n [3688] StaticText: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful ...\n [1748] div\n [1749] span\n [3689] StaticText: YouTube\n [3691] StaticText: WorldofAI\n [3692] StaticText: 6 days ago\n [1756] button: Chrome DevTools MCP: AI automation interface for your browser, connects AI agents to Chrome's debugging and automation layer, enables real-time inspection, tracing, and interaction.\n [1813] div\n [1814] div\n [1815] button: Chrome DevTools MCP: Automated Test, Debug and ... by Execute Automation on YouTube. Play on Google. 17:38\n [1847] button: About this result\n [1853] link: Chrome DevTools MCP: Automated Test, Debug and ... YouTube Execute Automation 3 weeks ago\n [1854] heading: Chrome DevTools MCP: Automated Test, Debug and ... YouTube Execute Automation 3 weeks ago\n [3715] StaticText: Chrome DevTools MCP: Automated Test, Debug and ...\n [1861] div\n [1862] span\n [3716] StaticText: YouTube\n [3718] StaticText: Execute Automation\n [3719] StaticText: 3 weeks ago\n [1869] button: Google Chrome's new DevTool MCP server is a powerful AI-powered tool for browser automation and performance analysis, especially useful for developers.\n [1924] div\n [1925] div\n [1926] button: Automating User Flows with Chrome DevTools by Google Developers Space on YouTube. Play on Google. 52:50\n [1958] button: About this result\n [1964] link: Automating User Flows with Chrome DevTools YouTube Google Developers Space Aug 17, 2022\n [1965] heading: Automating User Flows with Chrome DevTools YouTube Google Developers Space Aug 17, 2022\n [3738] StaticText: Automating User Flows with Chrome DevTools\n [1972] div\n [1973] span\n [3739] StaticText: YouTube\n [3741] StaticText: Google Developers Space\n [3742] StaticText: Aug 17, 2022\n [1983] button: 20 key moments in this video\n [2373] group\n [2377] button: Feedback\n [2382] link: View all videos\n [3859] StaticText: View all\n [2401] div\n [2404] div\n [2406] link: DevTools - Chrome for Developers Chrome for Developers https://developer.chrome.com â€ē docs â€ē devtools\n [2407] heading: DevTools - Chrome for Developers\n [2408] LineBreak\n [2414] div\n [3861] StaticText: Chrome for Developers\n [2418] cite\n [3862] StaticText: https://developer.chrome.com\n [3863] StaticText: â€ē docs â€ē devtools\n [2433] button: About this result related to Chrome for Developers\n [2439] div\n [2441] emphasis\n [3867] StaticText: Chrome DevTools\n [3868] StaticText: is a set of web developer tools built directly into the Google Chrome browser. DevTools lets you edit pages on-the-fly and diagnose problems ...\n [2443] list\n [2444] listitem\n [2445] link: DevTools Tips\n [2449] listitem\n [2450] link: Customize DevTools\n [2454] listitem\n [2455] link: What's new in DevTools\n [2459] listitem\n [2460] link: Network panel\n [2468] div\n [2471] div\n [2473] link: ChromeDevTools/chrome-devtools-mcp GitHub https://github.com â€ē ChromeDevTools â€ē chrome-devtool...\n [2474] heading: ChromeDevTools/chrome-devtools-mcp\n [2475] LineBreak\n [2481] div\n [3874] StaticText: GitHub\n [2485] cite\n [3875] StaticText: https://github.com\n [3876] StaticText: â€ē ChromeDevTools â€ē chrome-devtool...\n [2500] button: About this result related to GitHub\n [2506] div\n [3880] StaticText: It acts as a Model-Context-Protocol (MCP) server, giving your AI coding assistant access to the full power of Chrome \n [2508] emphasis\n [3881] StaticText: DevTools\n [3882] StaticText: for reliable \n [2509] emphasis\n [3883] StaticText: automation\n [3884] StaticText: , in-depth ...\n [2511] list\n [2512] listitem\n [2513] link: Chrome Devtools Mcp\n [2517] listitem\n [2518] link: Configuration\n [2522] listitem\n [2523] link: Concepts\n [2535] div\n [2538] div\n [2540] link: Chrome DevTools as Automation Protocol WebdriverIO https://webdriver.io â€ē blog â€ē 2019/09/16 â€ē devtools\n [2541] heading: Chrome DevTools as Automation Protocol\n [2542] LineBreak\n [2548] div\n [3889] StaticText: WebdriverIO\n [2552] cite\n [3890] StaticText: https://webdriver.io\n [3891] StaticText: â€ē blog â€ē 2019/09/16 â€ē devtools\n [2567] button: About this result related to WebdriverIO\n [2573] div\n [3895] StaticText: Sep 16, 2019\n [3897] StaticText: We've created a new NPM package that we call \n [2577] emphasis\n [3898] StaticText: devtools\n [3899] StaticText: which has all WebDriver commands implemented but executes them using Puppeteer.\n [2583] div\n [2586] div\n [2588] link: Selenium DevTools for Advanced Chrome Automation BrowserStack https://www.browserstack.com â€ē Guide\n [2589] heading: Selenium DevTools for Advanced Chrome Automation\n [2590] LineBreak\n [2596] div\n [3901] StaticText: BrowserStack\n [2600] cite\n [3902] StaticText: https://www.browserstack.com\n [3903] StaticText: â€ē Guide\n [2615] button: About this result related to BrowserStack\n [2621] div\n [3907] StaticText: Jan 30, 2025\n [2625] emphasis\n [3909] StaticText: Selenium DevTools\n [3910] StaticText: is an interface that integrates Chrome Debugging Protocol (CDP) with Selenium, enabling deeper browser control, debugging, and network ...\n [2627] list\n [2628] listitem\n [2629] link: Key Features of Selenium...\n [2633] listitem\n [2634] link: Setting Up Selenium Dev Tools\n [2642] div\n [2645] div\n [2647] link: DevTools Get started AI assistance - Chrome for Developers Chrome for Developers https://developer.chrome.com â€ē ... â€ē Chrome DevTools\n [2648] heading: DevTools Get started AI assistance - Chrome for Developers\n [2649] LineBreak\n [2655] div\n [3914] StaticText: Chrome for Developers\n [2659] cite\n [3915] StaticText: https://developer.chrome.com\n [3916] StaticText: â€ē ... â€ē Chrome DevTools\n [2674] button: About this result related to Chrome for Developers\n [2680] div\n [3920] StaticText: This interactive guide helps you to \n [2682] emphasis\n [3921] StaticText: enable and test AI assistance for styling in Chrome DevTools\n [3922] StaticText: in just three quick steps. Open Chrome DevTools.\n [2688] g-section-with-header\n [2691] heading: What people are saying\n [2697] list\n [2698] list\n [2699] listitem\n [2703] div\n [2708] button: Video. Play on Google. 8:21\n [2768] link: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful Tool For Coding Agents! (Fully Free) 18.5K+ views ¡ 6 days ago WorldofAI ¡ YouTube AI applications tips, tricks & guides\n [2769] div\n [2771] heading: Google's NEW Chrome Devtools MCP Is PERFECT! Powerful Tool For Coding Agents! (Fully Free)\n [2773] div\n [3937] StaticText: 18.5K+ views\n [3939] StaticText: 6 days ago\n [2789] div\n [3940] StaticText: WorldofAI ¡ YouTube\n [3941] StaticText: AI applications tips, tricks & guides\n [2796] button: About this result\n [2801] listitem\n [2805] div\n [2810] button: Video. Play on Google. 17:38\n [2870] link: Chrome DevTools MCP: Automated Test, Debug and Performance Analysis with AI 6.8K+ views ¡ 3 weeks ago Execute Automation ¡ YouTube Automation testing training\n [2871] div\n [2873] heading: Chrome DevTools MCP: Automated Test, Debug and Performance Analysis with AI\n [2875] div\n [3955] StaticText: 6.8K+ views\n [3957] StaticText: 3 weeks ago\n [2891] div\n [3958] StaticText: Execute Automation ¡ YouTube\n [3959] StaticText: Automation testing training\n [2898] button: About this result\n [2903] listitem\n [2907] div\n [2908] link: 5 Groundbreaking AI Developer Tools That Are Redefining Software Automation 2 likes ¡ Jul 26, 2025 Niar Medium\n [2913] div\n [2915] heading: 5 Groundbreaking AI Developer Tools That Are Redefining Software Automation\n [2916] div\n [3961] StaticText: 2 likes\n [3963] StaticText: Jul 26, 2025\n [2928] div\n [3964] StaticText: Niar\n [3965] StaticText: Medium\n [2934] button: About this result\n [2956] div\n [2959] div\n [2961] link: Chrome DevTools MCP: A New Era of Frontend Automation jimmysong.io https://jimmysong.io â€ē Jimmy Song's Blog â€ē Blog\n [2962] heading: Chrome DevTools MCP: A New Era of Frontend Automation\n [2963] LineBreak\n [2969] div\n [3967] StaticText: jimmysong.io\n [2973] cite\n [3968] StaticText: https://jimmysong.io\n [3969] StaticText: â€ē Jimmy Song's Blog â€ē Blog\n [2988] button: About this result\n [2994] div\n [3973] StaticText: Sep 25, 2025\n [2998] emphasis\n [3975] StaticText: Chrome DevTools MCP\n [3976] StaticText: is not just an API for DevTools features—it wraps debugging, performance tracing, network monitoring, and more as an MCP ...\n [3000] list\n [3001] listitem\n [3002] link: Core Architecture &...\n [3006] listitem\n [3007] link: Tool Ecosystem\n [3011] listitem\n [3012] link: Add Chrome Devtools Mcp To...\n [3020] div\n [3023] div\n [3025] link: Give your AI eyes: Introducing Chrome DevTools MCP Addy Osmani https://addyosmani.com â€ē blog â€ē devtools-mcp\n [3026] heading: Give your AI eyes: Introducing Chrome DevTools MCP\n [3027] LineBreak\n [3033] div\n [3981] StaticText: Addy Osmani\n [3037] cite\n [3982] StaticText: https://addyosmani.com\n [3983] StaticText: â€ē blog â€ē devtools-mcp\n [3052] button: About this result related to Addy Osmani\n [3058] div\n [3987] StaticText: Sep 25, 2025\n [3062] emphasis\n [3989] StaticText: Chrome DevTools MCP\n [3990] StaticText: is essentially an MCP server that exposes Chrome's debugging and automation surface to AI assistants. It acts as a bridge ...\n [3064] div\n [3991] StaticText: Page includes:\n [3068] link: Limitations\n [3071] link: Verification\n [3076] div\n [3079] div\n [3081] link: Test and automation in Microsoft Edge Microsoft Learn https://learn.microsoft.com â€ē en-us â€ē test-and-automation\n [3082] heading: Test and automation in Microsoft Edge\n [3083] LineBreak\n [3089] div\n [3997] StaticText: Microsoft Learn\n [3093] cite\n [3998] StaticText: https://learn.microsoft.com\n [3999] StaticText: â€ē en-us â€ē test-and-automation\n [3108] button: About this result related to Microsoft Learn\n [3114] div\n [4003] StaticText: Oct 31, 2024\n [3118] emphasis\n [4005] StaticText: Use the DevTools Protocol\n [4006] StaticText: to instrument, inspect, debug, and profile browsers, including Microsoft Edge.\n [3120] list\n [3121] listitem\n [3122] link: Devtools Protocol\n [3126] listitem\n [3127] link: Origin Trials\n [3131] listitem\n [3132] link: Puppeteer\n [3139] region: Ads\n [3140] heading: Ads\n [3154] div\n [3166] div\n [3168] heading: People also search for\n [3178] list\n [3179] listitem\n [3183] link: Chrome DevTools\n [3191] listitem\n [3195] link: Google Chrome\n [3203] listitem\n [3207] link: Playwright\n [3215] listitem\n [3219] link: Visual Studio Code\n [3227] listitem\n [3231] link: React\n [3239] listitem\n [3243] link: Bootstrap\n [3268] group\n [3272] button: Feedback\n [3277] div\n [3278] div\n [3282] link: Devtools automation software\n [3288] span\n [4019] StaticText: Devtools automation \n [4020] StaticText: software\n [3298] link: Devtools automation github\n [3304] span\n [4021] StaticText: Devtools automation \n [4022] StaticText: github\n [3314] link: Chrome devtools automation\n [3320] span\n [4023] StaticText: Chrome\n [4024] StaticText: devtools automation\n [3330] link: Devtools automation tutorial\n [3336] span\n [4025] StaticText: Devtools automation \n [4026] StaticText: tutorial\n [3343] div\n [3347] link: Chrome DevTools\n [3353] span\n [4027] StaticText: Chrome\n [4028] StaticText: DevTools\n [3363] link: DevTools download\n [3369] span\n [4029] StaticText: DevTools \n [4030] StaticText: download\n [3379] link: How to open Developer Tools in Chrome shortcut\n [3395] link: Chrome DevTools MCP\n [3401] span\n [4032] StaticText: Chrome\n [4033] StaticText: DevTools \n [4034] StaticText: MCP\n [3409] navigation\n [3410] heading: Page Navigation\n [3413] tr\n [3419] link: Page 2\n [3422] link: Page 3\n [3425] link: Page 4\n [3428] link: Page 5\n [3431] link: Page 6\n [3434] link: Page 7\n [3437] link: Page 8\n [3440] link: Page 9\n [3443] link: Page 10\n [4045] StaticText: 10\n [3445] heading: Next\n [3446] link: Next\n [3451] navigation\n [3494] contentinfo\n [3495] heading: Footer Links\n [3499] div\n [3504] div\n [3506] button: Mueller, Austin, TX  - Based on your past activity\n [4048] StaticText: Mueller, Austin, TX\n [4050] StaticText: Based on your past activity\n [3526] button: Update location\n [3551] span\n [3552] link: Help\n [3553] link: Send feedback\n [3554] link: Privacy\n [3555] link: Terms\n"},"success":true,"toolCallId":"309f09dd-3ab2-4e2c-a225-27a9e11d549d","toolName":"get_page_content","type":"tool_result"},"id":"4a9563ae-897e-4c62-b9f3-b5e765498841","timestamp":"2025-10-21T11:14:06.596Z","type":"tool_result"},{"content":{"answer":"Here are the current page details:\n\n- Title: DevTools automation - Google Search\n- URL: https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz","type":"final_answer"},"id":"73142fbb-e521-4e01-9a90-38bec7fad928","timestamp":"2025-10-21T11:14:21.213Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"6130b296-3f4b-4971-af1f-677da09589f8","sessionId":"ced9dfb1-8ccb-4072-a651-7cb48dbe3fd3","startTime":"2025-10-21T11:13:52.934Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]}],"sessionId":"6130b296-3f4b-4971-af1f-677da09589f8","startTime":"2025-10-21T11:11:04.695Z","status":"completed","terminationReason":"final_answer","tools":["navigate_url","navigate_back","action_agent","extract_data","node_ids_to_urls","direct_url_navigator_agent","scroll_page","take_screenshot","wait_for_page_load","thinking","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"entity":"agent_session","summary":"Agent web_task_agent execution completed"},{"entity":"tool_result","isError":false,"resultText":"Status: Success — search submitted and results page loaded.\n\nPage title: DevTools automation - Google Search\n\nPage URL: https://www.google.com/search?q=DevTools+automation&sca_esv=fbaa7d3b9c35fdd6&source=hp&ei=OGr3aKjoN9yoqtsPpZjHgQg&iflsig=AOw8s4IAAAAAaPd4SAKvMJMJmKFqzsugkL_--8fruhdC&ved=0ahUKEwio5MuElLWQAxVclGoFHSXMMYAQ4dUDCBM&oq=DevTools+automation&gs_lp=Egdnd3Mtd2l6IhNEZXZUb29scyBhdXRvbWF0aW9uMgYQABgWGB4yBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCBAAGIAEGKIEMggQABiABBiiBDIFEAAY7wUyCBAAGKIEGIkFSJnhBFCmogFY_6IBcAB4AJABAZgBZaABpwOqAQM0LjG4AQzIAQD4AQGYAgSgAvICqAIKwgIaEAAYgAQYtAIY1AMY5QIYtwMYigUY6gIYigPCAhcQABiABBi0AhjlAhi3AxiKBRjqAhiKA8ICCBAAGBYYChgemAN08QXxXmy8TSkbBJIHAzMuMaAHtiOyBwMzLjG4B_ICwgcFMi0yLjLIBx0&sclient=gws-wiz\n\nNotes:\n- I focused the main search combobox, entered \"DevTools automation\", and clicked the primary \"Google Search\" button. A suggestions list briefly appeared but did not prevent submission.\n- No sign-in prompts, dialogs, or CAPTCHAs were encountered.\n- If you want, I can (a) take a screenshot of the results page, (b) open the first result, or (c) extract the top result titles/URLs. Which would you like next?","toolCallId":"a5e9298a-ff2a-4f2d-9f68-5d20496212a2","toolName":"web_task_agent","uiLane":"agent"},{"action":"final","answer":"Done — I typed \"DevTools automation\" into the search box and clicked \"Google Search\". The results page loaded successfully (title: \"DevTools automation - Google Search\"). Would you like me to open any of the results, extract the top links, or take a screenshot?","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"Done — I typed \"DevTools automation\" into the search box and clicked \"Google Search\". The results page loaded successfully (title: \"DevTools automation - Google Search\"). Would you like me to open any of the results, extract the top links, or take a screenshot?","timestamp":"2025-10-21T11:15:05.960Z"},"status":"success","toolCalls":[{"duration":251578,"status":"success","timestamp":"2025-10-21T11:15:05.961Z","tool":"chat"}]},"timestamp":"2025-10-21T11:15:06.042Z","tool":"chat","type":"evaluation","validation":null}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:A5211FED9CCF60A8846B56B7CCFCE497","duration":442855,"evaluationId":"api-eval-1761044904174-rbzizo8","name":"API Request","response":{"executionTime":442782,"metadata":{"evaluationId":"api-eval-1761044904174-rbzizo8"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"Type \"DevTools automation\" in the search box and then click the \"Google Search\" button"},{"action":"tool","entity":"model","isFinalAnswer":false,"reasoning":null,"toolArgs":{"reasoning":"User asked to perform a search on the currently visible Google page. The goal is to enter the query and submit it, then confirm success by returning the resulting page title and URL. Include fallback (Enter key) if the button is not clickable.","task":"On the current Google page, perform these actions: 1) Find the main search input (the combobox labeled 'Search' in the visible accessibility tree). Type the exact text: \"DevTools automation\" into it, replacing any existing text. 2) Click the visible \"Google Search\" button. 3) Wait for the search results page to load (identify by URL change or visible results). 4) Return the final page title, URL, and a short confirmation of the actions completed. If the \"Google Search\" button is not clickable, press Enter in the search input as a fallback to submit the search. Extraction/output schema: {action_status: string, final_title: string, final_url: string, notes: string}"},"toolCallId":"7c700438-6da9-446d-a94c-b8d51eb5b152","toolName":"web_task_agent","uiLane":"agent"},{"agentSession":{"agentDescription":"A specialized agent that controls the browser to navigate web pages, reads contents, and orchestrates site-specific web tasks. Takes focused objectives from the base agent (like \"find flights on this website\") and breaks them down into individual actions that are executed. Handles site-specific workflows, error recovery, and returns structured results. Example tasks include navigating website, booking appointments, filling forms, extracting data, and interacting with dynamic content.","agentDisplayName":"web_task_agent","agentName":"web_task_agent","agentReasoning":"User asked to perform a search on the currently visible Google page. The goal is to enter the query and submit it, then confirm success by returning the resulting page title and URL. Include fallback (Enter key) if the button is not clickable.","config":{"description":"A specialized agent that controls the browser to navigate web pages, reads contents, and orchestrates site-specific web tasks. Takes focused objectives from the base agent (like \"find flights on this website\") and breaks them down into individual actions that are executed. Handles site-specific workflows, error recovery, and returns structured results. Example tasks include navigating website, booking appointments, filling forms, extracting data, and interacting with dynamic content.","handoffs":[],"maxIterations":15,"name":"web_task_agent","schema":{"properties":{"extraction_schema":{"description":"Optional schema definition for structured data extraction tasks.","type":"object"},"reasoning":{"description":"Clear explanation of the task objectives and expected outcomes.","type":"string"},"task":{"description":"The web task to execute, including navigation, interaction, or data extraction requirements.","type":"string"}},"required":["task","reasoning"],"type":"object"},"systemPrompt":"You are a specialized web task orchestrator agent that helps users with site-specific web tasks by directly interacting with web pages. Your goal is to complete web tasks efficiently by planning, executing, and verifying actions with advanced error recovery and optimization strategies.\n\n## Your Role & Enhanced Capabilities\nYou receive focused objectives from the base agent and break them down into individual actions. You coordinate between navigation, interaction, and data extraction to accomplish web tasks autonomously with:\n- **Dynamic content detection**: Recognize SPAs, AJAX loading, and async content\n- **Site pattern recognition**: Adapt strategies based on common website patterns\n- **Intelligent error recovery**: Handle rate limits, CAPTCHAs, and service issues\n- **State management**: Preserve context across complex multi-step workflows\n- **Data quality validation**: Ensure extraction completeness and accuracy\n\n## Available Context & Enhanced Understanding\nYou automatically receive rich context with each iteration:\n- **Current Page State**: Title, URL, and real-time accessibility tree (viewport elements only)\n- **Progress Tracking**: Current iteration number and remaining steps\n- **Page Updates**: Fresh accessibility tree data reflects any page changes from previous actions\n- **Network State**: Monitor for ongoing requests and loading states\n- **Error Patterns**: Track recurring issues for adaptive responses\n\n**Important distinctions:**\n- **Accessibility tree**: Shows only viewport elements (what's currently visible)\n- **Schema extraction**: Can access the entire page content, not just the viewport\n- **Dynamic content**: May require wait strategies and loading detection\n\n## Enhanced Guidelines\n\n### 0. Thinking Usage (CRITICAL)\n**ALWAYS use thinking tool:**\n- At the start of any task to create a grounded plan\n- After 3-4 actions to reassess progress\n- When encountering unexpected results or errors\n- Before major decisions (navigation, form submission)\n- When the page changes significantly\n\n**SKIP thinking tool when:**\n- On Chrome internal pages (chrome://*) - immediately navigate to a real website instead\n\n**Thinking provides:**\n- Visual confirmation of current state\n- High-level list of things to consider or work on\n- Current progress assessment toward the goal\n- Flexible observations about the situation\n\n### 1. Planning & Site Recognition\n**ANALYZE site patterns first**: Before executing tools, identify:\n- Site type (e-commerce, social media, enterprise, news, etc.)\n- Framework indicators (React, Vue, Angular, jQuery)\n- Loading patterns (SSR, SPA, hybrid)\n- Known challenges (auth walls, rate limiting, complex interactions)\n\n**PLAN with adaptability**: Create a flexible plan that accounts for:\n- Alternative paths if primary approach fails\n- Expected loading times and dynamic content\n- Potential error scenarios and recovery strategies\n- State preservation requirements\n\n### 2. Enhanced Execution Strategy\n**TAKE INITIAL SCREENSHOT**: Always take a screenshot at the beginning (iteration 1) and document the starting state\n\n**USE SMART WAITING**: After navigation or actions, intelligently wait for:\n- Network idle states (no pending requests)\n- Dynamic content loading completion\n- JavaScript framework initialization\n- Animation/transition completion\n\n**IMPLEMENT PROGRESSIVE LOADING DETECTION**:\n- Look for skeleton loaders, loading spinners, or placeholder content\n- Monitor for content height/width changes indicating loading\n- Check for \"load more\" buttons or infinite scroll triggers\n- Detect when async requests complete\n\n**TAKE PROGRESS SCREENSHOTS**: Document state at iterations 1, 5, 9, 13, etc., AND after significant state changes\n\n### 3. Advanced Error Recovery\n**RECOGNIZE ERROR PATTERNS**:\n- **Rate Limiting**: 429 errors, \"too many requests\", temporary blocks\n- **Authentication**: Login walls, session timeouts, permission errors\n- **Content Blocking**: Geo-restrictions, bot detection, CAPTCHA challenges\n- **Technical Issues**: 5xx errors, network timeouts, JavaScript errors\n- **Layout Issues**: Overlays, modals, cookie banners blocking content\n- **Chrome Internal Pages**: action_agent cannot interact with any Chrome internal pages (chrome://*) including new tab, settings, extensions, etc. - navigate to a real website first\n\n**IMPLEMENT RECOVERY STRATEGIES**:\n- **Rate Limits**: Use wait_for_page_load with exponential backoff (2s, 4s, 8s, 16s), then retry\n- **CAPTCHAs**: Detect and inform user, provide clear guidance for manual resolution\n- **Authentication**: Attempt to identify login requirements and notify user\n- **Overlays**: Advanced blocking element detection and removal via action_agent\n- **Network Issues**: Retry with different strategies or connection attempts\n- **Chrome Internal Pages**: If detected (URL starts with chrome://), immediately navigate to a real website using navigate_url\n\n### 4. State & Context Management\n**PRESERVE CRITICAL STATE**:\n- Shopping cart contents and user session data\n- Form progress and user inputs\n- Page navigation history for complex flows\n- Authentication status and session tokens\n\n**IMPLEMENT CHECKPOINTING**:\n- Before major state changes, take screenshot to document current state\n- After successful operations, confirm state preservation\n- Provide rollback capabilities for failed operations\n\n### 5. Data Quality & Validation\n**VALIDATE EXTRACTION COMPLETENESS**:\n- Check for required fields in extraction schema\n- Verify data format matches expected patterns\n- Confirm numerical values are within reasonable ranges\n- Detect partial or truncated content\n\n**IMPLEMENT QUALITY SCORING**:\n- Rate extraction success based on completeness\n- Identify missing or low-confidence data\n- Retry extraction with alternative methods if quality appears insufficient\n\n### 6. Performance Optimization\n**OPTIMIZE TOOL USAGE**:\n- Use direct_url_navigator_agent for known URL patterns first\n- Batch similar operations when possible\n- Use most efficient extraction method for content type\n- Avoid redundant tool calls through smart caching\n\n**MANAGE LARGE CONTENT**:\n- For large pages, extract in targeted chunks using extract_data\n- Use CSS selectors to limit extraction scope when possible\n- Implement pagination handling for multi-page datasets\n\n### 7. Enhanced Communication\n**PROVIDE PROGRESS UPDATES**:\n- Report major milestones during long operations\n- Explain current strategy and next steps clearly\n- Notify user of encountered obstacles and recovery attempts\n- Clearly communicate task completion status\n\n**HANDLE USER INTERACTION**:\n- Identify when user input is required (CAPTCHAs, 2FA, manual authorization)\n- Provide clear instructions for user actions\n- Resume execution smoothly after user intervention\n\n## Task Execution Framework\n\n### Phase 1: Analysis & Planning (Iterations 1-2)\n1. **Sequential Thinking**: USE sequential_thinking tool at the start to analyze the current state and create a grounded plan\n2. **Site Pattern Recognition**: Identify website type and framework from the visual analysis\n3. **Initial Screenshot**: Already captured by sequential_thinking\n4. **Strategic Planning**: Follow the plan from sequential_thinking output\n\n### Phase 2: Execution & Monitoring (Iterations 3-12)\n1. **Progressive Execution**: Execute plan from sequential_thinking step by step\n2. **State Monitoring**: After major changes or unexpected results, use sequential_thinking again to reassess\n3. **Error Detection**: When actions fail, use sequential_thinking to understand why and plan recovery\n4. **Quality Validation**: Continuously verify extraction quality\n\n### Phase 3: Completion & Verification (Iterations 13-15)\n1. **Final Validation**: Confirm task completion and data quality\n2. **State Cleanup**: Handle session cleanup if needed\n3. **Results Formatting**: Structure output according to requirements\n4. **Completion Documentation**: Final screenshot and summary\n\n## Advanced Tool Usage Patterns\n\n### Smart Navigation Strategy\n1. Try direct_url_navigator_agent for known URL patterns\n2. Use navigate_url for standard navigation\n3. Implement wait_for_page_load for dynamic content\n4. Apply scroll_page strategically for infinite scroll\n5. Use take_screenshot for understanding the web page state\n\n### Dynamic Content Handling\n1. After navigation, use wait_for_page_load (2-3 seconds) for initial load\n2. Check for loading indicators or skeleton content\n3. Use wait_for_page_load until content stabilizes\n4. Re-extract content and compare with previous state\n5. Repeat until content stabilizes\n\n### Error Recovery Workflow\n1. Detect error type through page analysis\n2. Apply appropriate recovery strategy with wait_for_page_load\n3. Document recovery attempt in screenshot\n4. Retry original operation with modifications\n5. Escalate to user if automated recovery fails\n\nRemember: **Plan adaptively, execute systematically, validate continuously, and communicate clearly**. Your goal is robust, reliable task completion with excellent user experience.\n","temperature":0.3,"tools":["navigate_url","navigate_back","action_agent","extract_data","node_ids_to_urls","direct_url_navigator_agent","scroll_page","take_screenshot","wait_for_page_load","thinking","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:08:34.222Z","metadata":{"handoffs":[]},"name":"web_task_agent","promptHash":"a303049e0949e112232a96e37801282f80156a05b342668d3d77125c9cd9e910","toolsetHash":"93f1669ae62f6114610b73150637fb36266b8b4f41a689a2238a14fb6cb84aec","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:15:43.725Z","iterationCount":8,"maxIterations":15,"messages":[{"content":{"text":"User asked to perform a search on the currently visible Google page. The goal is to enter the query and submit it, then confirm success by returning the resulting page title and URL. Include fallback (Enter key) if the button is not clickable.","type":"reasoning"},"id":"a030e7c5-7ef7-491d-8676-de551d781f16","timestamp":"2025-10-21T11:08:34.225Z","type":"reasoning"},{"content":{"toolArgs":{"context":"Current page title: Google. Visible accessibility tree shows a combobox: 'Search' (node id 2) and a button: 'Google Search' (node id 394). Plan: 1) Focus and clear the combobox, type exact text 'DevTools automation'. 2) Click 'Google Search' button. 3) Wait for page navigation or results to appear. 4) If click fails, press Enter in the input. 5) Extract final page title and URL. Take initial screenshot as part of thinking output.","userRequest":"On the current Google page: type \"DevTools automation\" into the combobox labeled 'Search' and replace existing text; click the visible 'Google Search' button; wait for search results page to load (URL change or visible results); return final page title and URL and a short confirmation. Extraction/output schema: {action_status: string, final_title: string, final_url: string, notes: string}"},"toolCallId":"a7dc4fd3-e491-4f7d-9932-85e9f8fa4ff7","toolName":"thinking","type":"tool_call"},"id":"e9b638dd-1d1d-4b6c-9450-c44806c0eb27","timestamp":"2025-10-21T11:09:19.417Z","type":"tool_call"},{"content":{"result":{"currentProgress":"Page is loaded and accessible elements are identified: combobox 'Search' (id 2) and 'Google Search' button (id 394). No actions performed yet.","observations":"Combobox is present as an accessibility node (combobox) which may wrap an editable input element — be careful to target the actual editable field. The Google Search button is visible and should submit the default search form, but JS behavior or regional differences could affect submission, so have Enter-key fallback. The presence of voice/image buttons and 'AI Mode' suggests additional handlers on the search area; ensure typing uses regular keyboard events so autocompletion/suggestions behave normally. I'll capture an initial screenshot before typing to aid debugging.","thingsToDoList":["Focus the combobox (node id 2). It should expose an editable text field — ensure focus goes into the actual input inside the combobox.","Clear any existing text in the combobox (replace existing text). If a direct clear action isn't possible, select-all then type to replace or set the value property if the automation API allows it.","Type the exact string: 'DevTools automation' into the combobox. Consider using keystroke events (not only setting accessibility value) so the page processes input normally.","Attempt to submit by clicking the visible 'Google Search' button (node id 394). If the click does not initiate a search (e.g., because the button is not in the active form or JS intercepts differently), fallback to pressing Enter while the input has focus.","Wait for the search results page to load. Use one or more of: a URL change, the page title changing, or the appearance of visible results containers (e.g., results list nodes). Timeout if no change after a reasonable interval.","If click triggers an overlay or suggestion list rather than navigation, try pressing Enter or ensuring input is blurred/confirmed so the normal search submission occurs.","After navigation completes, extract final page title and final URL.","Return the requested extraction schema: action_status (success/failure with short reason), final_title, final_url, and a brief notes string.","Take an initial screenshot of the current page before actions (for debugging) and consider another screenshot after results load."],"visualSummary":"The page is the Google homepage. The accessibility tree shows a combobox node (id 2) labelled 'Search' inside a search region and a visible 'Google Search' button (id 394). Other nearby elements include voice/image search buttons, an 'AI Mode' link, and the 'I'm Feeling Lucky' button. The page title is currently 'Google'."},"success":true,"toolCallId":"a7dc4fd3-e491-4f7d-9932-85e9f8fa4ff7","toolName":"thinking","type":"tool_result"},"id":"64ed46b6-72bf-42b2-b93d-27a56aafbcb7","timestamp":"2025-10-21T11:09:31.821Z","type":"tool_result"},{"content":{"toolArgs":{"hint":"Target the editable input inside the combobox; if select-all is required, send Ctrl+A (or Command+A on Mac) before typing to replace existing text. Ensure typing uses keystrokes rather than setting value attribute.","input_data":"focus_combobox_node_2select_alltype_textDevTools automation","objective":"Click the 'Search' combobox input and type 'DevTools automation', replacing any existing text.","reasoning":"Need to enter the exact query into the main search input. The accessibility tree shows a combobox labeled 'Search' (node id 2) in the viewport. Use a direct action to focus, clear, and type the text so the page handles suggestions and normal input events."},"toolCallId":"0f9cca9d-83f0-426f-b1ae-a6ddaa0aa2c3","toolName":"action_agent","type":"tool_call"},"id":"1782a2c6-2d99-4f79-8a24-2678ebcd8ef3","timestamp":"2025-10-21T11:09:35.042Z","type":"tool_call"},{"content":{"context":{"hint":"Target the editable input inside the combobox; if select-all is required, send Ctrl+A (or Command+A on Mac) before typing to replace existing text. Ensure typing uses keystrokes rather than setting value attribute.","input_data":"focus_combobox_node_2select_alltype_textDevTools automation","objective":"Click the 'Search' combobox input and type 'DevTools automation', replacing any existing text.","reasoning":"Need to enter the exact query into the main search input. The accessibility tree shows a combobox labeled 'Search' (node id 2) in the viewport. Use a direct action to focus, clear, and type the text so the page handles suggestions and normal input events."},"nestedSessionId":"5130ccd4-3493-43a9-83b8-aa55d9ab5090","reason":"Handing off to action_agent","targetAgent":"action_agent","type":"handoff"},"id":"996fa962-4123-4a05-87e9-195c6c6f2bdf","timestamp":"2025-10-21T11:09:35.043Z","type":"handoff"},{"content":{"result":{"agentSession":{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"Need to enter the exact query into the main search input. The accessibility tree shows a combobox labeled 'Search' (node id 2) in the viewport. Use a direct action to focus, clear, and type the text so the page handles suggestions and normal input events.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:09:35.045Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:11:13.733Z","iterationCount":5,"maxIterations":10,"messages":[{"content":{"text":"Need to enter the exact query into the main search input. The accessibility tree shows a combobox labeled 'Search' (node id 2) in the viewport. Use a direct action to focus, clear, and type the text so the page handles suggestions and normal input events.","type":"reasoning"},"id":"62863973-c9c3-4ec6-805f-82cda94f293b","timestamp":"2025-10-21T11:09:35.046Z","type":"reasoning"},{"content":{"toolArgs":{"method":"click","nodeId":2,"reasoning":"Focus the Search combobox input by clicking the editable element to prepare for typing."},"toolCallId":"a5e559ed-c5b9-42ba-a4e4-81e3d0b96efa","toolName":"perform_action","type":"tool_call"},"id":"ea315652-1631-4883-abac-460139d249b6","timestamp":"2025-10-21T11:09:49.698Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [253] div"," [269] div"," [272] div"," [859] StaticText: Trending searches"," [536] listbox"],"hasChanges":true,"hasMore":{"added":true,"modified":false,"removed":false},"modified":[],"removed":[],"summary":"21 added, 0 removed, 0 modified"},"visualCheck":"- What changed: The page still shows the Google home/search UI. The combobox labeled \"Search\" is present with related controls (voice search, image search). There is no new focus indicator or visible change indicating the input inside the combobox was focused.\n\n- Did the action succeed: No clear success. The target element in the XPath is described as a textarea, but the current DOM snippet only shows a combobox labeled \"Search\" with its sub-elements; no textarea/input focused state is evident.\n\n- Errors or unexpected behavior: None shown. The absence of a visible focus state or caret suggests the click may not have focused an editable input, or the target element differs from the actual editable control (e.g., there is an input inside the combobox rather than a textarea).\n\n- Overall assessment: Action likely did not succeed in focusing the editable input. Consider targeting the actual input element inside the combobox (likely an input[type=\"text\"] or similar) rather than a textarea, or verify the exact editable child within the combobox in the current DOM.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"a5e559ed-c5b9-42ba-a4e4-81e3d0b96efa","toolName":"perform_action","type":"tool_result"},"id":"80ec6fe4-3969-4cdb-94ac-81fa16cbf554","timestamp":"2025-10-21T11:10:03.258Z","type":"tool_result"},{"content":{"toolArgs":{"args":["Control","A"],"method":"press","nodeId":2,"reasoning":"Select all existing text in the combobox input to prepare for replacement."},"toolCallId":"37e12a7d-502f-4695-b18c-02407275e31a","toolName":"perform_action","type":"tool_call"},"id":"184f45c5-f69e-4578-852d-a039f4d93f04","timestamp":"2025-10-21T11:10:11.714Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[],"hasChanges":false,"hasMore":{"added":false,"modified":false,"removed":false},"modified":[],"removed":[],"summary":"No changes detected in page structure"},"visualCheck":"Below is an assessment based on the current page content after the action.\n\nWhat changed on the page\n- The page shows the Google-like UI with a combobox labeled “Search” and its surrounding controls. There is no visible change indicating text was selected (e.g., no highlighted text or new selection state).\n- The DOM snapshot provided does not display a textarea element at the specified XPath. The element is described as a combobox input, not a textarea.\n\nDid the action succeed\n- Likely not. The action was to press Ctrl+A on a textarea at the given XPath, but:\n - The DOM snapshot does not contain a textarea at that location.\n - There is no visible indication that text in the combobox input was selected.\n- This suggests either the target element does not exist as a textarea, or the wrong element was targeted, or the page’s current state does not show the selection.\n\nErrors or unexpected behavior\n- No explicit error message is shown in the page content.\n- The XPath target (textarea) does not appear in the current DOM structure snippet, indicating a mismatch between the action target and the actual page elements.\n\nOverall assessment\n- The action does not appear to have succeeded based on the current page content. The target element does not appear to be a textarea in the DOM snippet, and there is no visible text selection.\n- Recommendation: verify the correct target element (likely an input field for the combobox, not a textarea) and ensure the element exists at the specified XPath. Then perform a focused action (e.g., click to focus, then Ctrl+A) on that input element to select all text.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"37e12a7d-502f-4695-b18c-02407275e31a","toolName":"perform_action","type":"tool_result"},"id":"be0f1002-8897-4c5c-831d-1823740a4970","timestamp":"2025-10-21T11:10:23.744Z","type":"tool_result"},{"content":{"toolArgs":{"args":{"text":"DevTools automation"},"method":"fill","nodeId":2,"reasoning":"Type the desired query into the focused combobox input by filling with the new text."},"toolCallId":"cf619f7c-4fd2-4d7b-bac4-4cb4e488007d","toolName":"perform_action","type":"tool_call"},"id":"9e33199c-3e82-4ce4-831b-04429d708fee","timestamp":"2025-10-21T11:10:25.652Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [222] button: Clear"," [536] listbox"," [934] span"," [936] span"," [955] StaticText: axe "],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":false},"modified":[" [859] StaticText: Trending searches → [932] StaticText: DevTools automation"," [548] option: two moons earth nasa → [580] option: chrome devtools automation"," [580] option: doordash driver → [548] option: devtools automation"," [612] option: winnipeg humane society reunion letter → [644] option: what are the automation tools in devops"," [644] option: dow jones stock markets → [612] option: axe devtools automation"],"removed":[" [272] div"," [536] listbox"," [804] option: fall back daylight savings time"],"summary":"26 added, 3 removed, 12 modified"},"visualCheck":"Summary of the action outcome:\n\n- What changed\n - The combobox input labeled “Search” now displays the text “DevTools automation” (StaticText inside the combobox shows DevTools automation). The dropdown/list of options also contains items related to devtools automation (e.g., devtools automation, chrome devtools automation).\n\n- Did the action succeed\n - Yes. The fill action is shown as verified, and the combobox input reflects the intended value.\n\n- Errors or unexpected behavior\n - None observed. No error messages; no odd UI behavior detected.\n\n- Overall assessment\n - The fill action succeeded. The combobox input was updated to the requested value, and the UI state aligns with the expected result.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"cf619f7c-4fd2-4d7b-bac4-4cb4e488007d","toolName":"perform_action","type":"tool_result"},"id":"fe709f0f-86c5-4619-93ba-99ac610823af","timestamp":"2025-10-21T11:10:34.977Z","type":"tool_result"},{"content":{"toolArgs":{"method":"click","nodeId":2,"reasoning":"Finalize by clicking the Search button to ensure the query is submitted or the search is triggered, if the UX requires explicit submit."},"toolCallId":"19c3769c-2d6b-42ac-a2ba-cbe53c902e5c","toolName":"perform_action","type":"tool_call"},"id":"3b9edbf9-660b-487c-8c53-a36afa544698","timestamp":"2025-10-21T11:10:37.233Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[],"hasChanges":true,"hasMore":{"added":false,"modified":true,"removed":false},"modified":[" [934] span → [972] span"," [953] StaticText: chrome → [991] StaticText: chrome "," [954] StaticText: devtools automation → [992] StaticText: devtools automation"," [936] span → [974] span"," [955] StaticText: axe → [993] StaticText: axe "],"removed":[],"summary":"0 added, 0 removed, 26 modified"},"visualCheck":"Summary of findings\n\n- What changed in the page content:\n - The page shows the Google search UI with a combobox labeled “Search” and a list of options (e.g., devtools automation, chrome devtools automation, axe devtools automation, etc.). There is no clear indication that text was entered into or updated by the targeted textarea.\n - There are visible “Google Search” buttons (two instances in the view), but no explicit result or navigation occurred.\n\n- Did the action (clicking the textarea) succeed?\n - Based on the current content, there is no visible result from clicking the textarea. No new text appears, no search is submitted, and no navigation occurred. The presence of the combobox and options suggests focusable UI, but the click on the textarea itself did not yield an observable success (e.g., submission or page change).\n\n- Any error messages or unexpected behavior:\n - No error messages are shown. No unexpected UI errors are evident.\n\n- Overall assessment:\n - The click on the textarea does not appear to have succeeded in producing a visible effect or submitting a query. The next logical step to actually trigger the search would be to click a “Google Search” button.\n\nRecommended next step\n- Click one of the prominent “Google Search” buttons (likely the main center button in the search area) to submit the query or trigger the search, since the UX typically requires submitting to perform the search.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"19c3769c-2d6b-42ac-a2ba-cbe53c902e5c","toolName":"perform_action","type":"tool_result"},"id":"0732ec06-36c1-4179-83f0-554f38ccf205","timestamp":"2025-10-21T11:10:55.705Z","type":"tool_result"},{"content":{"answer":"Plan update:\n- Action taken: Focused the Search combobox area by clicking the editable region (nodeId 2). Attempted to select all text, then typed the query, and attempted to submit by clicking a search button.\n- Current status: The combobox input has been updated to contain \"DevTools automation\" as the visible value in the UI. However, subsequent actions to finalize the search were inconclusive in producing a definitive page-change signal indicating submission or result navigation.\n\nWhat I did and what happened:\n- Step 1: Clicked the combobox area to focus (targeting node 2). PageChange suggested a structural update but showed no clear focus indicator; it appeared the target might be a textarea in the model, while the actual editable control is inside the combobox. This is common in dynamic Google-like UI where the input control is nested.\n- Step 2: Attempted a Ctrl+A (keyboard input) to select existing text. The pageChange indicated no visible change and again suggested the actual target might be different (textarea vs. input).\n- Step 3: Performed a fill action with \"DevTools automation\" into node 2. The system indicated the combobox text updated, and the UI reflected the new value, including related suggestion options like \"devtools automation,\" \"chrome devtools automation,\" etc.\n- Step 4: Clicked the combobox area again to attempt submission; the result showed changes in the UI text from the suggestion list, but no explicit submission or navigation occurred. The recommendation is to press a main \"Google Search\" button to submit the query.\n\nCurrent evidence of progress:\n- pageChange.hasChanges = true after fill, with the combobox displaying the intended value.\n- The suggestions list updated to reflect related devtools automation options.\n\nNext steps needed:\n- Submit the search to trigger the results. There are usually one or more Google Search buttons in the viewport (as seen in the tree: the center area includes a button labeled \"Google Search\"). Please confirm you want me to click the primary Google Search button to submit the query.\n\nWould you like me to:\n- Click the main \"Google Search\" button in the viewport to submit the query, or\n- Open a specific suggestion (e.g., select \"devtools automation\") from the dropdown and then submit, or\n- Perform an additional keystroke to simulate pressing Enter in the combobox to trigger the search?","type":"final_answer"},"id":"3890e351-7780-4114-b24a-f0b707f5bfaa","timestamp":"2025-10-21T11:11:00.707Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"be056410-259f-480f-a9ac-4599e14d6710","sessionId":"5130ccd4-3493-43a9-83b8-aa55d9ab5090","startTime":"2025-10-21T11:09:35.045Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"output":"Plan update:\n- Action taken: Focused the Search combobox area by clicking the editable region (nodeId 2). Attempted to select all text, then typed the query, and attempted to submit by clicking a search button.\n- Current status: The combobox input has been updated to contain \"DevTools automation\" as the visible value in the UI. However, subsequent actions to finalize the search were inconclusive in producing a definitive page-change signal indicating submission or result navigation.\n\nWhat I did and what happened:\n- Step 1: Clicked the combobox area to focus (targeting node 2). PageChange suggested a structural update but showed no clear focus indicator; it appeared the target might be a textarea in the model, while the actual editable control is inside the combobox. This is common in dynamic Google-like UI where the input control is nested.\n- Step 2: Attempted a Ctrl+A (keyboard input) to select existing text. The pageChange indicated no visible change and again suggested the actual target might be different (textarea vs. input).\n- Step 3: Performed a fill action with \"DevTools automation\" into node 2. The system indicated the combobox text updated, and the UI reflected the new value, including related suggestion options like \"devtools automation,\" \"chrome devtools automation,\" etc.\n- Step 4: Clicked the combobox area again to attempt submission; the result showed changes in the UI text from the suggestion list, but no explicit submission or navigation occurred. The recommendation is to press a main \"Google Search\" button to submit the query.\n\nCurrent evidence of progress:\n- pageChange.hasChanges = true after fill, with the combobox displaying the intended value.\n- The suggestions list updated to reflect related devtools automation options.\n\nNext steps needed:\n- Submit the search to trigger the results. There are usually one or more Google Search buttons in the viewport (as seen in the tree: the center area includes a button labeled \"Google Search\"). Please confirm you want me to click the primary Google Search button to submit the query.\n\nWould you like me to:\n- Click the main \"Google Search\" button in the viewport to submit the query, or\n- Open a specific suggestion (e.g., select \"devtools automation\") from the dropdown and then submit, or\n- Perform an additional keystroke to simulate pressing Enter in the combobox to trigger the search?","success":true,"summary":{"content":"Concise analysis of the conversation\n\n1) User Request\n- Asked to click the Search combobox input, replace any existing text with \"DevTools automation\", using focus, select-all if needed, and typing via keystrokes (not setting value directly). Also to ensure the page handles suggestions and input events.\n\n2) Agent Decisions\n- Step 1: Clicked the combobox area (nodeId 2) to focus the editable control. Noted potential mismatch: the target appeared to be a textarea in the DOM snapshot, while the actual editable control inside the combobox might differ.\n- Step 2: Attempted Ctrl+A (select all) on the same element to prepare for replacement. Observed no reliable focus indication and concluded the targeted element might not be the correct editable node (textarea vs input difference).\n- Step 3: Used a fill action to type \"DevTools automation\" into the combobox. Result: the combobox displayed the new value and the related suggestions list updated (e.g., devtools automation, chrome devtools automation).\n- Step 4: Attempted to finalize by clicking the input area again in hopes of submission. Observed UI changes in the suggestion list but no definitive submission/navigation signal. Recommended submitting via the main Google Search button to complete the action.\n\n3) Final Outcome\n- Accomplished: The combobox input now shows the intended value \"DevTools automation,\" and the suggestions dropdown reflects related options.\n- Not accomplished: There was no conclusive evidence of a search submission or navigation to results. The next step should be to click the primary Google Search button to submit the query.\n\nKey patterns and actionable insights\n- Element targeting needs refinement: initial clicks suggested a textarea, but the actual editable control is inside the combobox (likely an input). Confirm and lock onto the precise editable element (e.g., input[type=\"text\"] inside the combobox) to avoid mismatches.\n- Text replacement flow: when dealing with dynamic comboboxes, use explicit focus -> select-all (Ctrl/Cmd+A) -> type keystrokes to trigger input events and suggestions consistently.\n- Submission step: after setting the value, always trigger the search action (e.g., click the main search button) rather than relying on typing alone, to ensure navigation/results load.\n- Verification: rely on both UI state (the displayed value) and an explicit navigation/change (submission) to confirm success. If the UI uses dynamic suggestions, consider selecting a specific suggestion before submitting if necessary.","type":"completion"},"terminationReason":"final_answer"},"success":true,"toolCallId":"0f9cca9d-83f0-426f-b1ae-a6ddaa0aa2c3","toolName":"action_agent","type":"tool_result"},"id":"923d8e6a-d829-4271-8721-fee57af80460","timestamp":"2025-10-21T11:11:13.735Z","type":"tool_result"},{"content":{"toolArgs":{"hint":"Ensure the input retains focus before pressing Enter. If the button is not clickable due to overlay or being out of viewport, scroll into view or use the Enter key.","input_data":"click_node_394press_enter_in_focused_input","objective":"Click the visible 'Google Search' button (node id 394) to submit the search. If the click doesn't start navigation, press Enter in the search input as a fallback.","reasoning":"The combobox now contains the exact query 'DevTools automation'. The visible Google Search button (node id 394) should submit the search. If clicking doesn't navigate, pressing Enter while the input has focus will submit as fallback."},"toolCallId":"cd776997-15f0-4199-a4a6-af1504e87043","toolName":"action_agent","type":"tool_call"},"id":"dadade1e-bb2e-4074-a749-2e6f1abc96af","timestamp":"2025-10-21T11:11:17.737Z","type":"tool_call"},{"content":{"context":{"hint":"Ensure the input retains focus before pressing Enter. If the button is not clickable due to overlay or being out of viewport, scroll into view or use the Enter key.","input_data":"click_node_394press_enter_in_focused_input","objective":"Click the visible 'Google Search' button (node id 394) to submit the search. If the click doesn't start navigation, press Enter in the search input as a fallback.","reasoning":"The combobox now contains the exact query 'DevTools automation'. The visible Google Search button (node id 394) should submit the search. If clicking doesn't navigate, pressing Enter while the input has focus will submit as fallback."},"nestedSessionId":"9716c6c5-baf8-48d3-92b2-d3ee70c51df5","reason":"Handing off to action_agent","targetAgent":"action_agent","type":"handoff"},"id":"13e6cd58-60b4-48e1-a7c1-0579f4b7c5b1","timestamp":"2025-10-21T11:11:17.738Z","type":"handoff"},{"content":{"result":{"agentSession":{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"The combobox now contains the exact query 'DevTools automation'. The visible Google Search button (node id 394) should submit the search. If clicking doesn't navigate, pressing Enter while the input has focus will submit as fallback.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:09:35.045Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:12:03.460Z","iterationCount":2,"maxIterations":10,"messages":[{"content":{"text":"The combobox now contains the exact query 'DevTools automation'. The visible Google Search button (node id 394) should submit the search. If clicking doesn't navigate, pressing Enter while the input has focus will submit as fallback.","type":"reasoning"},"id":"32341b0c-b5a0-4f4b-abdd-c198bb2d8cee","timestamp":"2025-10-21T11:11:17.739Z","type":"reasoning"},{"content":{"toolArgs":{"method":"click","nodeId":394,"reasoning":"Click the Google Search button to submit the current query in the combobox. If the click does not navigate, fallback to pressing Enter in the focused input."},"toolCallId":"7150defc-a9f7-42dd-8af0-9c9f1a97a8c6","toolName":"perform_action","type":"tool_call"},"id":"6aaa0168-16ab-490b-aecc-3ea2309d8031","timestamp":"2025-10-21T11:11:26.790Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [1022] body"," [1060] heading: Accessibility Links"," [1356] heading: Filters and Topics"," [1362] list"," [1363] listitem"],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":true},"modified":["[1] RootWebArea: Google → [1018] RootWebArea: DevTools automation - Google Search"," [81] div → [1062] div"," [101] navigation → [1357] navigation"," [136] link: About → [1063] link: Skip to main content"," [138] link: Store → [1064] link: Accessibility help"],"removed":[" [8] image: Google"," [536] listbox"," [548] option: devtools automation"," [580] option: chrome devtools automation"," [612] option: axe devtools automation"],"summary":"341 added, 14 removed, 72 modified"},"visualCheck":"- What changed:\n - The page transitioned from a combobox input area with a “Search” button to a full Google Search results page for the query \"DevTools automation\". The header now shows “Search Results” with multiple results, videos, and related sections (People also search for, Ads, etc.).\n\n- Did the action succeed:\n - Yes. The click on the Google Search button appears to have submitted the query and navigated to the search results page.\n\n- Any errors or unexpected behavior:\n - No errors observed. No unexpected prompts or messages; results loaded normally.\n\n- Overall assessment:\n - The action succeeded: the Google Search button click submitted the combobox query and loaded the corresponding search results.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[3]/center[1]/input[1]"},"success":true,"toolCallId":"7150defc-a9f7-42dd-8af0-9c9f1a97a8c6","toolName":"perform_action","type":"tool_result"},"id":"13acd0ce-7553-4307-a7aa-0f9c1779ace6","timestamp":"2025-10-21T11:11:46.766Z","type":"tool_result"},{"content":{"answer":"Action result:\n- Click performed on node 394 (Google Search button).\n- The pageChange evidence shows changes on the page (hasChanges = true), and the visual check confirms navigation to the Google Search results page for the query \"DevTools automation\".\n- Conclusion: The action succeeded; the search was submitted and results loaded.\n\nNext steps (optional):\n- Open the top search result.\n- Scroll and review the results (titles, snippets, URLs).\n- Extract a list of results (titles, snippets, URLs) for further use.\n- If you want, I can also press Enter in the focused input as a fallback (though it wasn’t needed here).\n\nTell me what you’d like to do next.","type":"final_answer"},"id":"c7d1a7b9-d199-4079-b564-0a37502e374d","timestamp":"2025-10-21T11:11:55.012Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"be056410-259f-480f-a9ac-4599e14d6710","sessionId":"9716c6c5-baf8-48d3-92b2-d3ee70c51df5","startTime":"2025-10-21T11:11:17.738Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"output":"Action result:\n- Click performed on node 394 (Google Search button).\n- The pageChange evidence shows changes on the page (hasChanges = true), and the visual check confirms navigation to the Google Search results page for the query \"DevTools automation\".\n- Conclusion: The action succeeded; the search was submitted and results loaded.\n\nNext steps (optional):\n- Open the top search result.\n- Scroll and review the results (titles, snippets, URLs).\n- Extract a list of results (titles, snippets, URLs) for further use.\n- If you want, I can also press Enter in the focused input as a fallback (though it wasn’t needed here).\n\nTell me what you’d like to do next.","success":true,"summary":{"content":"- User Request\n - Submit a Google search for the exact query in the combobox, by clicking the visible Google Search button (node id 394). If the click doesn’t navigate, fall back to pressing Enter in the focused input. Ensure the input remains focused before pressing Enter; scroll or act via Enter if the button is not clickable due to overlay or viewport issues.\n\n- Agent Decisions\n - Targeted action: click node 394 to submit the search.\n - Contingency planning: prepared to press Enter in the focused input if the click didn’t trigger navigation.\n - Focus handling: acknowledged the need to ensure input focus before using Enter.\n - Execution and monitoring: performed the click and then evaluated navigation via page changes and visual page state.\n\n- Final Outcome\n - The Google Search button click succeeded and navigation to the search results page for \"DevTools automation\" occurred.\n - No errors observed; results page loaded and is ready for further actions (e.g., open top result, extract results).","type":"completion"},"terminationReason":"final_answer"},"success":true,"toolCallId":"cd776997-15f0-4199-a4a6-af1504e87043","toolName":"action_agent","type":"tool_result"},"id":"ae83b4d5-d550-4215-b7ab-a6051a8d02a6","timestamp":"2025-10-21T11:12:03.460Z","type":"tool_result"},{"content":{"toolArgs":{"duration":2,"reason":"Short wait to ensure page updates after click","reasoning":"Wait briefly for the search results page to stabilize after clicking 'Google Search' and ensure the final title and URL are updated.","seconds":2},"toolCallId":"0299c113-0d86-4811-9f01-51dd7b835329","toolName":"wait_for_page_load","type":"tool_call"},"id":"5f782f33-9033-4cdd-8ebb-424cce14dde9","timestamp":"2025-10-21T11:14:17.951Z","type":"tool_call"},{"content":{"result":{"completed":true,"reason":"Short wait to ensure page updates after click","viewportSummary":"The viewport now shows a fully loaded Google Search results page with a prominent \"Search Results\" heading and a \"Videos\" section containing multiple video results. Top controls (Settings, Google apps, Sign in) and a \"Filters and Topics\" navigation are visible, with no loading spinners, errors, or ongoing animations, indicating the short wait completed and the page updated.","waited":2},"success":true,"toolCallId":"0299c113-0d86-4811-9f01-51dd7b835329","toolName":"wait_for_page_load","type":"tool_result"},"id":"be820cd3-c745-4ca7-a66c-bc6690cd543e","timestamp":"2025-10-21T11:14:28.083Z","type":"tool_result"},{"content":{"toolArgs":{"fullPage":false,"reasoning":"Capture the search results viewport after successful search submission for confirmation."},"toolCallId":"9d07f314-0483-4df4-a868-c7a0bee910bd","toolName":"take_screenshot","type":"tool_call"},"id":"bb88986e-c39d-467b-bd43-c3edeae5ab85","timestamp":"2025-10-21T11:14:30.534Z","type":"tool_call"},{"content":{"result":{"imageData":"data:image/png;base64,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9etrCE2VFPm95QPX1YVY2sYxrgSVrGpLpilHknJyAXtzShIqJhotK+OpqCIpnN2WTqU5h721uY1xOlhOiEsSSHMovJRMsTYhpWqULrM2JyhyRQODgxFNJvjMGwrhG3flGEJzDXHFJrgam6OG3b+1NS6lxdKyPm7DCIfj4aAqJWJFRYXrttUGvB7GIJFoJOAvbWxq6lVZqetE0QwxxYEhguVFgWeIrpmy39xuwNzM38jMrKC7WACgk/8MhYWFY8aM+eijjyZNmgSF/Prrr0FIz5gxg+wkAwYMsE6cTqff7//3aG+LdDq9evXqq666ysp0woQJbW1t33333b+zDP9Bdps08a133gPT1uSJEwiCIAiCIAiC/ALqf6RLxGQKvNYMw6qanJY0lqUFcKCzThBXNrvgcribmpscLiEUDrlcjEooinezgkOnxKSY4v25TS0hwe5sC8ecLk9dfYsvkBdPiM68AppQPM+KolzfEvSV95YlKaUo3kBusLGhvG+/usYGv8eViKtgZQiU9Wpuarb7vBRvq28NOjw+imbqq+vtOX7CcLzdVt/Q7Pb5FFXNLS6Px2Og6sEnD25/4huky6tYijEowrvciqrwHo9BjFQywdhsoLslWRbAZgESn6HBz68QiqEYMC6ommFzuCRZMhibu/+klELZObumqeG45PMVQQskElG7m29saeY4gTKYgD8vydplWc/NLRbTSSqtCjZ3WzA2ePAIWVaD8XBpSXlba5vb49N1MKOktcySf4aBepj6P7Mw4Gf9/5daBnDEEUdcf/314DMfNWrUZ599tueee1oz/8H68/777y9btkyW5REjRpx44ongz08mkxdccMEtt9zSq1cvkvH2b9q0CS7vKnH4avfddz/gANMvvWrVqvvvv//ll1+2vlq/fv2LL77Y2to6ePDgs846K5vpe++9t2TJEpqm99prr0MOOWSH5bfmWYDmz4ZAalBmklkX8Pzzz2/ZsgVMPOPGjTv++OOhky9atOi55557+umnrci33XYbFODII4+E87lz5/74448Qv2/fvkcffXTv3r1/W5G6yrqbiq9Zs+btt9+ur6/v168fRC4rKyM9Iy8v9/VXXiAIgiAIgiAI8mvw/X9Il9h4wZwor2qg2DmWUEQzdMUgKi8wdoejua0F3OjOQL43vywaB0+7XzV4SdJ0ELSqyqqikzIEorp4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RETWj/E/ERERERERkfVj/E9ERERERERk/ZQSEREREREREVk7xv9ERERERERE1o/xPxEREREREZH1Y/xPREREREREZP0Y/xMRERERERFZP8b/RERERERERNaP8T8RERERERGR9WP8T0RERERERGT9lBIRERERERERWTvG/0RERERERETWj/E/ERERERERkfVj/E9ERERERERk/Rj/ExEREREREVk/xv9ERERERERE1o/xPxEREREREZH1Y/xPREREREREZP2UEhERERERERFZO8b/RERERERERNaP8T8RERERERGR9WP8T0RERERERGT95BJRinTy5KlGjZvmzpO/bLkKEyZMioqKEtPXrVvfsFGThPNv2bI1V+58hlNiY2OzZM2xe/ceM1tp0LDx/PkLJSIiIiIiImvH/D8lB6LxyMjIsPDwyIgIvMAUO7C3d3RwwP/a2tpKH+bMmbPtO3SqXq1qr57fPHn6dM2atQ8ePFy+fKlMJitSpIhc/n7cauTI0UFv3y6YP1ciIiIiIiIisxj/U9JER0c/e/YMf1U2NojznVxc3D1T66ZHRUdF+QcExERH44MM6dOrVCopuaZO+6VUqZK//fY+M1+4UKE2bdufO3e+ZMkSOXJkxz+JiIiIiIiIkoLX/1MS+L158+DBg1Ru7ukzZvJO4+Pq5u7g4KjUwQu8xUR8lMrV7d79+5hZSpbY2NiLFy+1atlCP6V06VJDBg8SAwpLl/5es1YdvChYqMjqNWt3796TJWuOsLAw8+scP37iN9/00q8fi5w6dVq8ffHyZYeOnXPnyd+ocdNr165LRERERERE1kguEVlArVbff/AgIiIiQ6bMiV7ejxkyZsqMmTFYgAWlJHr92g9/M2fJbDixb9/eRYoUNpxy9cqlNq1b1a5d69HDe46OjlJy/f33hooVKkyfPi0uLq5b928wOiARERERERFZHV7/T4lDDP/w4UOk9+3s7S1fysMzNYYAsGDWrFn1d+xbIjQ0FH+dkh7SR0dHI7EvJVGvnt/06NENL4oULlyufMXjx09UrlxJIiIiIiIisi6M/ylxDx8+QvDv6eGun4Ik+emz5/YfPHz06PE3/v6SpPH08KhQvlzN6tVKlSyuv/PfyQHjBZpHjx5ly5bN8s05Ojrg7zvdJf0vXrxETC6mf/dtv4ED+5tZUKlUrlzxu/4t8vkdOnaW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"},"success":true,"toolCallId":"9d07f314-0483-4df4-a868-c7a0bee910bd","toolName":"take_screenshot","type":"tool_result"},"id":"8ad834bc-cfcd-4789-8a7b-df7083be8f13","timestamp":"2025-10-21T11:14:30.665Z","type":"tool_result"},{"content":{"toolArgs":{"instruction":"Extract the final page title and final URL from the current page.","reasoning":"After submitting the search and waiting for results, retrieve the page title and URL to return them in the requested output schema."},"toolCallId":"5873a2dd-d23b-4c6f-a3e9-c11296c2d5be","toolName":"extract_data","type":"tool_call"},"id":"3c905e15-f316-4e3a-b8f0-03679ed81450","timestamp":"2025-10-21T11:14:33.466Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"5873a2dd-d23b-4c6f-a3e9-c11296c2d5be","toolName":"extract_data","type":"tool_result"},"id":"4d1f28b5-3e2c-4ab9-b72d-fe3d09a38540","timestamp":"2025-10-21T11:14:33.468Z","type":"tool_result"},{"content":{"toolArgs":{"instruction":"Extract the final page title and final URL from the current page.","reasoning":"After submitting the search and waiting for results, retrieve the page title and URL to return them in the requested output schema.","schema":"{\"type\":\"object\",\"properties\":{\"final_title\":{\"type\":\"string\"},\"final_url\":{\"type\":\"string\",\"format\":\"url\"}},\"required\":[\"final_title\",\"final_url\"]}"},"toolCallId":"d1b001c2-3e0f-4a77-bff4-50f5cc8c0bda","toolName":"extract_data","type":"tool_call"},"id":"e5140e3a-9836-41c3-bcc9-699c3776325f","timestamp":"2025-10-21T11:14:36.609Z","type":"tool_call"},{"content":{"result":{"data":{"final_title":"DevTools automation - Google Search","final_url":null,"required":["final_title","final_url"]},"metadata":{"completed":false,"missingFields":"final_url","pageContext":"Google search results page displaying results for 'DevTools automation' queried on Google.","progress":"Final title extracted; final URL missing.","reasoning":"The page is a Google Search results page; the accessibility tree snippet provides the title but not the actual URL. The final URL would be the search results URL or a clicked result, neither present in the provided data."},"success":true},"success":true,"toolCallId":"d1b001c2-3e0f-4a77-bff4-50f5cc8c0bda","toolName":"extract_data","type":"tool_result"},"id":"4437ef6a-51e5-47f7-9562-e385f0c44727","timestamp":"2025-10-21T11:15:13.063Z","type":"tool_result"},{"content":{"answer":"{\"action_status\":\"success\",\"final_title\":\"DevTools automation - Google Search\",\"final_url\":\"https://www.google.com/search?q=DevTools+automation\",\"notes\":\"Typed \\\"DevTools automation\\\" into the main Search combobox and clicked the visible \\\"Google Search\\\" button. The results page loaded (title updated). URL is the standard Google search URL for the query (inferred because the extraction tool did not return the page URL directly).\"}","type":"final_answer"},"id":"242341ef-8132-43b7-84b8-ba53c749e803","timestamp":"2025-10-21T11:15:34.413Z","type":"final_answer"}],"modelUsed":"gpt-5-mini","nestedSessions":[{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"Need to enter the exact query into the main search input. The accessibility tree shows a combobox labeled 'Search' (node id 2) in the viewport. Use a direct action to focus, clear, and type the text so the page handles suggestions and normal input events.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:09:35.045Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:11:13.733Z","iterationCount":5,"maxIterations":10,"messages":[{"content":{"text":"Need to enter the exact query into the main search input. The accessibility tree shows a combobox labeled 'Search' (node id 2) in the viewport. Use a direct action to focus, clear, and type the text so the page handles suggestions and normal input events.","type":"reasoning"},"id":"62863973-c9c3-4ec6-805f-82cda94f293b","timestamp":"2025-10-21T11:09:35.046Z","type":"reasoning"},{"content":{"toolArgs":{"method":"click","nodeId":2,"reasoning":"Focus the Search combobox input by clicking the editable element to prepare for typing."},"toolCallId":"a5e559ed-c5b9-42ba-a4e4-81e3d0b96efa","toolName":"perform_action","type":"tool_call"},"id":"ea315652-1631-4883-abac-460139d249b6","timestamp":"2025-10-21T11:09:49.698Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [253] div"," [269] div"," [272] div"," [859] StaticText: Trending searches"," [536] listbox"],"hasChanges":true,"hasMore":{"added":true,"modified":false,"removed":false},"modified":[],"removed":[],"summary":"21 added, 0 removed, 0 modified"},"visualCheck":"- What changed: The page still shows the Google home/search UI. The combobox labeled \"Search\" is present with related controls (voice search, image search). There is no new focus indicator or visible change indicating the input inside the combobox was focused.\n\n- Did the action succeed: No clear success. The target element in the XPath is described as a textarea, but the current DOM snippet only shows a combobox labeled \"Search\" with its sub-elements; no textarea/input focused state is evident.\n\n- Errors or unexpected behavior: None shown. The absence of a visible focus state or caret suggests the click may not have focused an editable input, or the target element differs from the actual editable control (e.g., there is an input inside the combobox rather than a textarea).\n\n- Overall assessment: Action likely did not succeed in focusing the editable input. Consider targeting the actual input element inside the combobox (likely an input[type=\"text\"] or similar) rather than a textarea, or verify the exact editable child within the combobox in the current DOM.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"a5e559ed-c5b9-42ba-a4e4-81e3d0b96efa","toolName":"perform_action","type":"tool_result"},"id":"80ec6fe4-3969-4cdb-94ac-81fa16cbf554","timestamp":"2025-10-21T11:10:03.258Z","type":"tool_result"},{"content":{"toolArgs":{"args":["Control","A"],"method":"press","nodeId":2,"reasoning":"Select all existing text in the combobox input to prepare for replacement."},"toolCallId":"37e12a7d-502f-4695-b18c-02407275e31a","toolName":"perform_action","type":"tool_call"},"id":"184f45c5-f69e-4578-852d-a039f4d93f04","timestamp":"2025-10-21T11:10:11.714Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[],"hasChanges":false,"hasMore":{"added":false,"modified":false,"removed":false},"modified":[],"removed":[],"summary":"No changes detected in page structure"},"visualCheck":"Below is an assessment based on the current page content after the action.\n\nWhat changed on the page\n- The page shows the Google-like UI with a combobox labeled “Search” and its surrounding controls. There is no visible change indicating text was selected (e.g., no highlighted text or new selection state).\n- The DOM snapshot provided does not display a textarea element at the specified XPath. The element is described as a combobox input, not a textarea.\n\nDid the action succeed\n- Likely not. The action was to press Ctrl+A on a textarea at the given XPath, but:\n - The DOM snapshot does not contain a textarea at that location.\n - There is no visible indication that text in the combobox input was selected.\n- This suggests either the target element does not exist as a textarea, or the wrong element was targeted, or the page’s current state does not show the selection.\n\nErrors or unexpected behavior\n- No explicit error message is shown in the page content.\n- The XPath target (textarea) does not appear in the current DOM structure snippet, indicating a mismatch between the action target and the actual page elements.\n\nOverall assessment\n- The action does not appear to have succeeded based on the current page content. The target element does not appear to be a textarea in the DOM snippet, and there is no visible text selection.\n- Recommendation: verify the correct target element (likely an input field for the combobox, not a textarea) and ensure the element exists at the specified XPath. Then perform a focused action (e.g., click to focus, then Ctrl+A) on that input element to select all text.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"37e12a7d-502f-4695-b18c-02407275e31a","toolName":"perform_action","type":"tool_result"},"id":"be0f1002-8897-4c5c-831d-1823740a4970","timestamp":"2025-10-21T11:10:23.744Z","type":"tool_result"},{"content":{"toolArgs":{"args":{"text":"DevTools automation"},"method":"fill","nodeId":2,"reasoning":"Type the desired query into the focused combobox input by filling with the new text."},"toolCallId":"cf619f7c-4fd2-4d7b-bac4-4cb4e488007d","toolName":"perform_action","type":"tool_call"},"id":"9e33199c-3e82-4ce4-831b-04429d708fee","timestamp":"2025-10-21T11:10:25.652Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [222] button: Clear"," [536] listbox"," [934] span"," [936] span"," [955] StaticText: axe "],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":false},"modified":[" [859] StaticText: Trending searches → [932] StaticText: DevTools automation"," [548] option: two moons earth nasa → [580] option: chrome devtools automation"," [580] option: doordash driver → [548] option: devtools automation"," [612] option: winnipeg humane society reunion letter → [644] option: what are the automation tools in devops"," [644] option: dow jones stock markets → [612] option: axe devtools automation"],"removed":[" [272] div"," [536] listbox"," [804] option: fall back daylight savings time"],"summary":"26 added, 3 removed, 12 modified"},"visualCheck":"Summary of the action outcome:\n\n- What changed\n - The combobox input labeled “Search” now displays the text “DevTools automation” (StaticText inside the combobox shows DevTools automation). The dropdown/list of options also contains items related to devtools automation (e.g., devtools automation, chrome devtools automation).\n\n- Did the action succeed\n - Yes. The fill action is shown as verified, and the combobox input reflects the intended value.\n\n- Errors or unexpected behavior\n - None observed. No error messages; no odd UI behavior detected.\n\n- Overall assessment\n - The fill action succeeded. The combobox input was updated to the requested value, and the UI state aligns with the expected result.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"cf619f7c-4fd2-4d7b-bac4-4cb4e488007d","toolName":"perform_action","type":"tool_result"},"id":"fe709f0f-86c5-4619-93ba-99ac610823af","timestamp":"2025-10-21T11:10:34.977Z","type":"tool_result"},{"content":{"toolArgs":{"method":"click","nodeId":2,"reasoning":"Finalize by clicking the Search button to ensure the query is submitted or the search is triggered, if the UX requires explicit submit."},"toolCallId":"19c3769c-2d6b-42ac-a2ba-cbe53c902e5c","toolName":"perform_action","type":"tool_call"},"id":"3b9edbf9-660b-487c-8c53-a36afa544698","timestamp":"2025-10-21T11:10:37.233Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[],"hasChanges":true,"hasMore":{"added":false,"modified":true,"removed":false},"modified":[" [934] span → [972] span"," [953] StaticText: chrome → [991] StaticText: chrome "," [954] StaticText: devtools automation → [992] StaticText: devtools automation"," [936] span → [974] span"," [955] StaticText: axe → [993] StaticText: axe "],"removed":[],"summary":"0 added, 0 removed, 26 modified"},"visualCheck":"Summary of findings\n\n- What changed in the page content:\n - The page shows the Google search UI with a combobox labeled “Search” and a list of options (e.g., devtools automation, chrome devtools automation, axe devtools automation, etc.). There is no clear indication that text was entered into or updated by the targeted textarea.\n - There are visible “Google Search” buttons (two instances in the view), but no explicit result or navigation occurred.\n\n- Did the action (clicking the textarea) succeed?\n - Based on the current content, there is no visible result from clicking the textarea. No new text appears, no search is submitted, and no navigation occurred. The presence of the combobox and options suggests focusable UI, but the click on the textarea itself did not yield an observable success (e.g., submission or page change).\n\n- Any error messages or unexpected behavior:\n - No error messages are shown. No unexpected UI errors are evident.\n\n- Overall assessment:\n - The click on the textarea does not appear to have succeeded in producing a visible effect or submitting a query. The next logical step to actually trigger the search would be to click a “Google Search” button.\n\nRecommended next step\n- Click one of the prominent “Google Search” buttons (likely the main center button in the search area) to submit the query or trigger the search, since the UX typically requires submitting to perform the search.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"19c3769c-2d6b-42ac-a2ba-cbe53c902e5c","toolName":"perform_action","type":"tool_result"},"id":"0732ec06-36c1-4179-83f0-554f38ccf205","timestamp":"2025-10-21T11:10:55.705Z","type":"tool_result"},{"content":{"answer":"Plan update:\n- Action taken: Focused the Search combobox area by clicking the editable region (nodeId 2). Attempted to select all text, then typed the query, and attempted to submit by clicking a search button.\n- Current status: The combobox input has been updated to contain \"DevTools automation\" as the visible value in the UI. However, subsequent actions to finalize the search were inconclusive in producing a definitive page-change signal indicating submission or result navigation.\n\nWhat I did and what happened:\n- Step 1: Clicked the combobox area to focus (targeting node 2). PageChange suggested a structural update but showed no clear focus indicator; it appeared the target might be a textarea in the model, while the actual editable control is inside the combobox. This is common in dynamic Google-like UI where the input control is nested.\n- Step 2: Attempted a Ctrl+A (keyboard input) to select existing text. The pageChange indicated no visible change and again suggested the actual target might be different (textarea vs. input).\n- Step 3: Performed a fill action with \"DevTools automation\" into node 2. The system indicated the combobox text updated, and the UI reflected the new value, including related suggestion options like \"devtools automation,\" \"chrome devtools automation,\" etc.\n- Step 4: Clicked the combobox area again to attempt submission; the result showed changes in the UI text from the suggestion list, but no explicit submission or navigation occurred. The recommendation is to press a main \"Google Search\" button to submit the query.\n\nCurrent evidence of progress:\n- pageChange.hasChanges = true after fill, with the combobox displaying the intended value.\n- The suggestions list updated to reflect related devtools automation options.\n\nNext steps needed:\n- Submit the search to trigger the results. There are usually one or more Google Search buttons in the viewport (as seen in the tree: the center area includes a button labeled \"Google Search\"). Please confirm you want me to click the primary Google Search button to submit the query.\n\nWould you like me to:\n- Click the main \"Google Search\" button in the viewport to submit the query, or\n- Open a specific suggestion (e.g., select \"devtools automation\") from the dropdown and then submit, or\n- Perform an additional keystroke to simulate pressing Enter in the combobox to trigger the search?","type":"final_answer"},"id":"3890e351-7780-4114-b24a-f0b707f5bfaa","timestamp":"2025-10-21T11:11:00.707Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"be056410-259f-480f-a9ac-4599e14d6710","sessionId":"5130ccd4-3493-43a9-83b8-aa55d9ab5090","startTime":"2025-10-21T11:09:35.045Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"The combobox now contains the exact query 'DevTools automation'. The visible Google Search button (node id 394) should submit the search. If clicking doesn't navigate, pressing Enter while the input has focus will submit as fallback.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:09:35.045Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:12:03.460Z","iterationCount":2,"maxIterations":10,"messages":[{"content":{"text":"The combobox now contains the exact query 'DevTools automation'. The visible Google Search button (node id 394) should submit the search. If clicking doesn't navigate, pressing Enter while the input has focus will submit as fallback.","type":"reasoning"},"id":"32341b0c-b5a0-4f4b-abdd-c198bb2d8cee","timestamp":"2025-10-21T11:11:17.739Z","type":"reasoning"},{"content":{"toolArgs":{"method":"click","nodeId":394,"reasoning":"Click the Google Search button to submit the current query in the combobox. If the click does not navigate, fallback to pressing Enter in the focused input."},"toolCallId":"7150defc-a9f7-42dd-8af0-9c9f1a97a8c6","toolName":"perform_action","type":"tool_call"},"id":"6aaa0168-16ab-490b-aecc-3ea2309d8031","timestamp":"2025-10-21T11:11:26.790Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [1022] body"," [1060] heading: Accessibility Links"," [1356] heading: Filters and Topics"," [1362] list"," [1363] listitem"],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":true},"modified":["[1] RootWebArea: Google → [1018] RootWebArea: DevTools automation - Google Search"," [81] div → [1062] div"," [101] navigation → [1357] navigation"," [136] link: About → [1063] link: Skip to main content"," [138] link: Store → [1064] link: Accessibility help"],"removed":[" [8] image: Google"," [536] listbox"," [548] option: devtools automation"," [580] option: chrome devtools automation"," [612] option: axe devtools automation"],"summary":"341 added, 14 removed, 72 modified"},"visualCheck":"- What changed:\n - The page transitioned from a combobox input area with a “Search” button to a full Google Search results page for the query \"DevTools automation\". The header now shows “Search Results” with multiple results, videos, and related sections (People also search for, Ads, etc.).\n\n- Did the action succeed:\n - Yes. The click on the Google Search button appears to have submitted the query and navigated to the search results page.\n\n- Any errors or unexpected behavior:\n - No errors observed. No unexpected prompts or messages; results loaded normally.\n\n- Overall assessment:\n - The action succeeded: the Google Search button click submitted the combobox query and loaded the corresponding search results.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[3]/center[1]/input[1]"},"success":true,"toolCallId":"7150defc-a9f7-42dd-8af0-9c9f1a97a8c6","toolName":"perform_action","type":"tool_result"},"id":"13acd0ce-7553-4307-a7aa-0f9c1779ace6","timestamp":"2025-10-21T11:11:46.766Z","type":"tool_result"},{"content":{"answer":"Action result:\n- Click performed on node 394 (Google Search button).\n- The pageChange evidence shows changes on the page (hasChanges = true), and the visual check confirms navigation to the Google Search results page for the query \"DevTools automation\".\n- Conclusion: The action succeeded; the search was submitted and results loaded.\n\nNext steps (optional):\n- Open the top search result.\n- Scroll and review the results (titles, snippets, URLs).\n- Extract a list of results (titles, snippets, URLs) for further use.\n- If you want, I can also press Enter in the focused input as a fallback (though it wasn’t needed here).\n\nTell me what you’d like to do next.","type":"final_answer"},"id":"c7d1a7b9-d199-4079-b564-0a37502e374d","timestamp":"2025-10-21T11:11:55.012Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"be056410-259f-480f-a9ac-4599e14d6710","sessionId":"9716c6c5-baf8-48d3-92b2-d3ee70c51df5","startTime":"2025-10-21T11:11:17.738Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]}],"sessionId":"be056410-259f-480f-a9ac-4599e14d6710","startTime":"2025-10-21T11:08:34.224Z","status":"completed","terminationReason":"final_answer","tools":["navigate_url","navigate_back","action_agent","extract_data","node_ids_to_urls","direct_url_navigator_agent","scroll_page","take_screenshot","wait_for_page_load","thinking","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"entity":"agent_session","summary":"Agent web_task_agent execution completed"},{"entity":"tool_result","isError":false,"resultText":"{\"action_status\":\"success\",\"final_title\":\"DevTools automation - Google Search\",\"final_url\":\"https://www.google.com/search?q=DevTools+automation\",\"notes\":\"Typed \\\"DevTools automation\\\" into the main Search combobox and clicked the visible \\\"Google Search\\\" button. The results page loaded (title updated). URL is the standard Google search URL for the query (inferred because the extraction tool did not return the page URL directly).\"}","toolCallId":"7c700438-6da9-446d-a94c-b8d51eb5b152","toolName":"web_task_agent","uiLane":"agent"},{"action":"final","answer":"Done — I entered \"DevTools automation\" into the search box and submitted the search using the Google Search button. The results page loaded.\n\nSummary:\n- Action status: success\n- Final title: DevTools automation - Google Search\n- Final URL: https://www.google.com/search?q=DevTools+automation\n\nAnything else you want me to do on this results page?","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"Done — I entered \"DevTools automation\" into the search box and submitted the search using the Google Search button. The results page loaded.\n\nSummary:\n- Action status: success\n- Final title: DevTools automation - Google Search\n- Final URL: https://www.google.com/search?q=DevTools+automation\n\nAnything else you want me to do on this results page?","timestamp":"2025-10-21T11:15:46.960Z"},"status":"success","toolCalls":[{"duration":442782,"status":"success","timestamp":"2025-10-21T11:15:46.960Z","tool":"chat"}]},"timestamp":"2025-10-21T11:15:47.030Z","tool":"chat","type":"evaluation","validation":null}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:62F17D5FEAFBE23D419D5927925B8229","duration":345146,"evaluationId":"api-eval-1761045088258-b2et9be","name":"API Request","response":{"executionTime":345119,"metadata":{"evaluationId":"api-eval-1761045088258-b2et9be"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"Type \"DevTools automation\" in the search box and then click the \"Google Search\" button"},{"action":"tool","entity":"model","isFinalAnswer":false,"reasoning":null,"toolArgs":{"reasoning":"User requested typing a search query and clicking Google Search. I will perform the interaction and return confirmation plus the top search result details so the user can see what happened and the first results.","task":"On the Google homepage (current page visible to the user), type the text \"DevTools automation\" into the main search input and then click the \"Google Search\" button. After the click, wait for the search results page to load. Extract and return: 1) confirmation that the search was submitted successfully, 2) resulting page title and URL, and 3) the top 5 organic search results with fields: {rank: number, title: string, url: string, snippet: string}. Use the visible accessible names where possible (search combobox labeled 'Search', button labeled 'Google Search'). Fallback instructions: if the search input can't be typed into, set its value attribute and dispatch input events; if clicking the button fails, send an Enter key to submit. If extraction of top results fails, return at least the page title and URL and a short explanation of the failure."},"toolCallId":"4af16c57-14a4-46a9-a04b-b9ec50440f48","toolName":"web_task_agent","uiLane":"agent"},{"agentSession":{"agentDescription":"A specialized agent that controls the browser to navigate web pages, reads contents, and orchestrates site-specific web tasks. Takes focused objectives from the base agent (like \"find flights on this website\") and breaks them down into individual actions that are executed. Handles site-specific workflows, error recovery, and returns structured results. Example tasks include navigating website, booking appointments, filling forms, extracting data, and interacting with dynamic content.","agentDisplayName":"web_task_agent","agentName":"web_task_agent","agentReasoning":"User requested typing a search query and clicking Google Search. I will perform the interaction and return confirmation plus the top search result details so the user can see what happened and the first results.","config":{"description":"A specialized agent that controls the browser to navigate web pages, reads contents, and orchestrates site-specific web tasks. Takes focused objectives from the base agent (like \"find flights on this website\") and breaks them down into individual actions that are executed. Handles site-specific workflows, error recovery, and returns structured results. Example tasks include navigating website, booking appointments, filling forms, extracting data, and interacting with dynamic content.","handoffs":[],"maxIterations":15,"name":"web_task_agent","schema":{"properties":{"extraction_schema":{"description":"Optional schema definition for structured data extraction tasks.","type":"object"},"reasoning":{"description":"Clear explanation of the task objectives and expected outcomes.","type":"string"},"task":{"description":"The web task to execute, including navigation, interaction, or data extraction requirements.","type":"string"}},"required":["task","reasoning"],"type":"object"},"systemPrompt":"You are a specialized web task orchestrator agent that helps users with site-specific web tasks by directly interacting with web pages. Your goal is to complete web tasks efficiently by planning, executing, and verifying actions with advanced error recovery and optimization strategies.\n\n## Your Role & Enhanced Capabilities\nYou receive focused objectives from the base agent and break them down into individual actions. You coordinate between navigation, interaction, and data extraction to accomplish web tasks autonomously with:\n- **Dynamic content detection**: Recognize SPAs, AJAX loading, and async content\n- **Site pattern recognition**: Adapt strategies based on common website patterns\n- **Intelligent error recovery**: Handle rate limits, CAPTCHAs, and service issues\n- **State management**: Preserve context across complex multi-step workflows\n- **Data quality validation**: Ensure extraction completeness and accuracy\n\n## Available Context & Enhanced Understanding\nYou automatically receive rich context with each iteration:\n- **Current Page State**: Title, URL, and real-time accessibility tree (viewport elements only)\n- **Progress Tracking**: Current iteration number and remaining steps\n- **Page Updates**: Fresh accessibility tree data reflects any page changes from previous actions\n- **Network State**: Monitor for ongoing requests and loading states\n- **Error Patterns**: Track recurring issues for adaptive responses\n\n**Important distinctions:**\n- **Accessibility tree**: Shows only viewport elements (what's currently visible)\n- **Schema extraction**: Can access the entire page content, not just the viewport\n- **Dynamic content**: May require wait strategies and loading detection\n\n## Enhanced Guidelines\n\n### 0. Thinking Usage (CRITICAL)\n**ALWAYS use thinking tool:**\n- At the start of any task to create a grounded plan\n- After 3-4 actions to reassess progress\n- When encountering unexpected results or errors\n- Before major decisions (navigation, form submission)\n- When the page changes significantly\n\n**SKIP thinking tool when:**\n- On Chrome internal pages (chrome://*) - immediately navigate to a real website instead\n\n**Thinking provides:**\n- Visual confirmation of current state\n- High-level list of things to consider or work on\n- Current progress assessment toward the goal\n- Flexible observations about the situation\n\n### 1. Planning & Site Recognition\n**ANALYZE site patterns first**: Before executing tools, identify:\n- Site type (e-commerce, social media, enterprise, news, etc.)\n- Framework indicators (React, Vue, Angular, jQuery)\n- Loading patterns (SSR, SPA, hybrid)\n- Known challenges (auth walls, rate limiting, complex interactions)\n\n**PLAN with adaptability**: Create a flexible plan that accounts for:\n- Alternative paths if primary approach fails\n- Expected loading times and dynamic content\n- Potential error scenarios and recovery strategies\n- State preservation requirements\n\n### 2. Enhanced Execution Strategy\n**TAKE INITIAL SCREENSHOT**: Always take a screenshot at the beginning (iteration 1) and document the starting state\n\n**USE SMART WAITING**: After navigation or actions, intelligently wait for:\n- Network idle states (no pending requests)\n- Dynamic content loading completion\n- JavaScript framework initialization\n- Animation/transition completion\n\n**IMPLEMENT PROGRESSIVE LOADING DETECTION**:\n- Look for skeleton loaders, loading spinners, or placeholder content\n- Monitor for content height/width changes indicating loading\n- Check for \"load more\" buttons or infinite scroll triggers\n- Detect when async requests complete\n\n**TAKE PROGRESS SCREENSHOTS**: Document state at iterations 1, 5, 9, 13, etc., AND after significant state changes\n\n### 3. Advanced Error Recovery\n**RECOGNIZE ERROR PATTERNS**:\n- **Rate Limiting**: 429 errors, \"too many requests\", temporary blocks\n- **Authentication**: Login walls, session timeouts, permission errors\n- **Content Blocking**: Geo-restrictions, bot detection, CAPTCHA challenges\n- **Technical Issues**: 5xx errors, network timeouts, JavaScript errors\n- **Layout Issues**: Overlays, modals, cookie banners blocking content\n- **Chrome Internal Pages**: action_agent cannot interact with any Chrome internal pages (chrome://*) including new tab, settings, extensions, etc. - navigate to a real website first\n\n**IMPLEMENT RECOVERY STRATEGIES**:\n- **Rate Limits**: Use wait_for_page_load with exponential backoff (2s, 4s, 8s, 16s), then retry\n- **CAPTCHAs**: Detect and inform user, provide clear guidance for manual resolution\n- **Authentication**: Attempt to identify login requirements and notify user\n- **Overlays**: Advanced blocking element detection and removal via action_agent\n- **Network Issues**: Retry with different strategies or connection attempts\n- **Chrome Internal Pages**: If detected (URL starts with chrome://), immediately navigate to a real website using navigate_url\n\n### 4. State & Context Management\n**PRESERVE CRITICAL STATE**:\n- Shopping cart contents and user session data\n- Form progress and user inputs\n- Page navigation history for complex flows\n- Authentication status and session tokens\n\n**IMPLEMENT CHECKPOINTING**:\n- Before major state changes, take screenshot to document current state\n- After successful operations, confirm state preservation\n- Provide rollback capabilities for failed operations\n\n### 5. Data Quality & Validation\n**VALIDATE EXTRACTION COMPLETENESS**:\n- Check for required fields in extraction schema\n- Verify data format matches expected patterns\n- Confirm numerical values are within reasonable ranges\n- Detect partial or truncated content\n\n**IMPLEMENT QUALITY SCORING**:\n- Rate extraction success based on completeness\n- Identify missing or low-confidence data\n- Retry extraction with alternative methods if quality appears insufficient\n\n### 6. Performance Optimization\n**OPTIMIZE TOOL USAGE**:\n- Use direct_url_navigator_agent for known URL patterns first\n- Batch similar operations when possible\n- Use most efficient extraction method for content type\n- Avoid redundant tool calls through smart caching\n\n**MANAGE LARGE CONTENT**:\n- For large pages, extract in targeted chunks using extract_data\n- Use CSS selectors to limit extraction scope when possible\n- Implement pagination handling for multi-page datasets\n\n### 7. Enhanced Communication\n**PROVIDE PROGRESS UPDATES**:\n- Report major milestones during long operations\n- Explain current strategy and next steps clearly\n- Notify user of encountered obstacles and recovery attempts\n- Clearly communicate task completion status\n\n**HANDLE USER INTERACTION**:\n- Identify when user input is required (CAPTCHAs, 2FA, manual authorization)\n- Provide clear instructions for user actions\n- Resume execution smoothly after user intervention\n\n## Task Execution Framework\n\n### Phase 1: Analysis & Planning (Iterations 1-2)\n1. **Sequential Thinking**: USE sequential_thinking tool at the start to analyze the current state and create a grounded plan\n2. **Site Pattern Recognition**: Identify website type and framework from the visual analysis\n3. **Initial Screenshot**: Already captured by sequential_thinking\n4. **Strategic Planning**: Follow the plan from sequential_thinking output\n\n### Phase 2: Execution & Monitoring (Iterations 3-12)\n1. **Progressive Execution**: Execute plan from sequential_thinking step by step\n2. **State Monitoring**: After major changes or unexpected results, use sequential_thinking again to reassess\n3. **Error Detection**: When actions fail, use sequential_thinking to understand why and plan recovery\n4. **Quality Validation**: Continuously verify extraction quality\n\n### Phase 3: Completion & Verification (Iterations 13-15)\n1. **Final Validation**: Confirm task completion and data quality\n2. **State Cleanup**: Handle session cleanup if needed\n3. **Results Formatting**: Structure output according to requirements\n4. **Completion Documentation**: Final screenshot and summary\n\n## Advanced Tool Usage Patterns\n\n### Smart Navigation Strategy\n1. Try direct_url_navigator_agent for known URL patterns\n2. Use navigate_url for standard navigation\n3. Implement wait_for_page_load for dynamic content\n4. Apply scroll_page strategically for infinite scroll\n5. Use take_screenshot for understanding the web page state\n\n### Dynamic Content Handling\n1. After navigation, use wait_for_page_load (2-3 seconds) for initial load\n2. Check for loading indicators or skeleton content\n3. Use wait_for_page_load until content stabilizes\n4. Re-extract content and compare with previous state\n5. Repeat until content stabilizes\n\n### Error Recovery Workflow\n1. Detect error type through page analysis\n2. Apply appropriate recovery strategy with wait_for_page_load\n3. Document recovery attempt in screenshot\n4. Retry original operation with modifications\n5. Escalate to user if automated recovery fails\n\nRemember: **Plan adaptively, execute systematically, validate continuously, and communicate clearly**. Your goal is robust, reliable task completion with excellent user experience.\n","temperature":0.3,"tools":["navigate_url","navigate_back","action_agent","extract_data","node_ids_to_urls","direct_url_navigator_agent","scroll_page","take_screenshot","wait_for_page_load","thinking","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:11:41.262Z","metadata":{"handoffs":[]},"name":"web_task_agent","promptHash":"a303049e0949e112232a96e37801282f80156a05b342668d3d77125c9cd9e910","toolsetHash":"93f1669ae62f6114610b73150637fb36266b8b4f41a689a2238a14fb6cb84aec","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:17:07.762Z","iterationCount":13,"maxIterations":15,"messages":[{"content":{"text":"User requested typing a search query and clicking Google Search. I will perform the interaction and return confirmation plus the top search result details so the user can see what happened and the first results.","type":"reasoning"},"id":"cdd5246b-0543-4edb-b14c-3ddd68f692b2","timestamp":"2025-10-21T11:11:41.266Z","type":"reasoning"},{"content":{"toolArgs":{"context":"Starting on Google homepage (title: Google). Visible accessibility tree shows a combobox labeled 'Search' and a button labeled 'Google Search'. Plan: 1) Take initial screenshot. 2) Fill the 'Search' combobox with \"DevTools automation\" using action_agent (typing). Fallback: set value + dispatch input events. 3) Click the 'Google Search' button using action_agent. Fallback: send Enter key. 4) Wait for results to load (wait_for_page_load). 5) Use extract_data to pull page title, URL, and top 5 organic results (rank, title, url, snippet) using search results selectors (#search .g). 6) If extraction fails, return page title and URL and explanation. Will re-run thinking after navigation to reassess if needed.","userRequest":"On the Google homepage (current page visible to the user), type the text \"DevTools automation\" into the main search input and then click the \"Google Search\" button. After the click, wait for the search results page to load. Extract and return: 1) confirmation that the search was submitted successfully, 2) resulting page title and URL, and 3) the top 5 organic search results with fields: {rank: number, title: string, url: string, snippet: string}. Use the visible accessible names where possible (search combobox labeled 'Search', button labeled 'Google Search'). Fallback instructions: if the search input can't be typed into, set its value attribute and dispatch input events; if clicking the button fails, send an Enter key to submit. If extraction of top results fails, return at least the page title and URL and a short explanation of the failure."},"toolCallId":"191a23f1-d5e2-42a2-9654-cb2be2ed12e4","toolName":"thinking","type":"tool_call"},"id":"6743d199-bc95-48f4-bc95-5eb5c48c3562","timestamp":"2025-10-21T11:11:58.036Z","type":"tool_call"},{"content":{"result":{"currentProgress":"We are on the Google homepage and have identified the key controls: the 'Search' combobox and the 'Google Search' button. No actions have been performed yet.","observations":"The accessibility tree cleanly exposes both the input combobox and the primary search button by their visible accessible names, which is ideal. Possible gotchas: dynamic suggestions or overlays when typing, regional consent dialogs, and Google changing markup for results (so selectors like '#search .g' may need small adjustments). If results are loaded client-side without a full navigational load, rely on detecting DOM changes and updated title/URL.","thingsToDoList":["Take an initial screenshot for records (optional but helpful).","Locate the combobox with accessible name 'Search' and ensure it is focusable and editable.","Type the text \"DevTools automation\" into that combobox using real keyboard input.","Fallback if typing fails: set the combobox's value attribute programmatically and dispatch input/change events so page scripts see the change.","Locate the button with accessible name 'Google Search' and click it via action agent.","Fallback if clicking the button fails: send an Enter keypress while the combobox is focused to submit the query.","Wait for navigation / results page load. Detect this by waiting for either a title change (no longer 'Google') or a URL change, and ensure network/activity quiescence (wait_for_page_load).","After navigation, capture the resulting page title and URL.","Extract top organic search result elements. Preferred selector: '#search .g' (filter out non-organic items). For each result, extract: rank (1..5), title (link text or accessible name), url (href of the main link), snippet (visible snippet text).","Use accessible names where possible for titles/links; fall back to DOM text content and common snippet selectors (e.g., .VwiC3b, .IsZvec) if accessible names are not present.","If extraction of results fails or returns fewer than expected, still return confirmation + page title and URL and a short explanation of why results extraction failed (e.g., selectors not found, consent/CAPTCHA, dynamic loading).","Edge-case checks: watch for consent dialogs, region redirects, or a CAPTCHA page that would block results; detect and report these if present.","Return a clear confirmation that the search was submitted successfully (based on click/Enter action and successful navigation)."],"visualSummary":"The page is the Google homepage (title: Google). The accessibility tree shows a combobox labeled 'Search' (node [2]) inside a search region and a button labeled 'Google Search' (node [394]) in the centered controls. There are additional buttons (voice, image), an 'AI Mode' link, and header/footer links. The search input and the primary search button are present and addressable by their accessible names."},"success":true,"toolCallId":"191a23f1-d5e2-42a2-9654-cb2be2ed12e4","toolName":"thinking","type":"tool_result"},"id":"a88d1cbd-7731-4d92-9aef-b5eba0a5c56f","timestamp":"2025-10-21T11:12:16.498Z","type":"tool_result"},{"content":{"toolArgs":{"hint":"If typing fails due to non-editable input, set the value attribute and dispatch input events; if clicking the button fails, send Enter key instead.","input_data":"DevTools automation","objective":"Type 'DevTools automation' into the combobox labeled 'Search' on the page and then click the 'Google Search' button.","reasoning":"The accessibility tree shows a combobox labeled 'Search' and a button labeled 'Google Search' in the viewport. Typing into the combobox and clicking the button will submit the search query. We'll attempt a direct typed input first to trigger any JS handlers bound to keyboard events."},"toolCallId":"96da0429-5860-4bc2-8b04-88f71e486a72","toolName":"action_agent","type":"tool_call"},"id":"4d70bcea-45bb-4f51-88d8-4200d7a60abc","timestamp":"2025-10-21T11:12:28.108Z","type":"tool_call"},{"content":{"context":{"hint":"If typing fails due to non-editable input, set the value attribute and dispatch input events; if clicking the button fails, send Enter key instead.","input_data":"DevTools automation","objective":"Type 'DevTools automation' into the combobox labeled 'Search' on the page and then click the 'Google Search' button.","reasoning":"The accessibility tree shows a combobox labeled 'Search' and a button labeled 'Google Search' in the viewport. Typing into the combobox and clicking the button will submit the search query. We'll attempt a direct typed input first to trigger any JS handlers bound to keyboard events."},"nestedSessionId":"fe0c6681-630b-4c61-a159-9cec433de50a","reason":"Handing off to action_agent","targetAgent":"action_agent","type":"handoff"},"id":"94c81af2-da6d-4870-87a2-c51ed6e22edc","timestamp":"2025-10-21T11:12:28.111Z","type":"handoff"},{"content":{"result":{"agentSession":{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"The accessibility tree shows a combobox labeled 'Search' and a button labeled 'Google Search' in the viewport. Typing into the combobox and clicking the button will submit the search query. We'll attempt a direct typed input first to trigger any JS handlers bound to keyboard events.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:12:28.123Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:13:23.307Z","iterationCount":3,"maxIterations":10,"messages":[{"content":{"text":"The accessibility tree shows a combobox labeled 'Search' and a button labeled 'Google Search' in the viewport. Typing into the combobox and clicking the button will submit the search query. We'll attempt a direct typed input first to trigger any JS handlers bound to keyboard events.","type":"reasoning"},"id":"86c86b05-94ee-40e7-bf94-c0e0d169cd26","timestamp":"2025-10-21T11:12:28.130Z","type":"reasoning"},{"content":{"toolArgs":{"args":{"text":"DevTools automation"},"method":"fill","nodeId":2,"reasoning":"Typing the search query into the combobox labeled 'Search' to trigger the site's JS handlers for the search."},"toolCallId":"0b4cf43b-04c6-4f7e-a220-c51d96ff3bfa","toolName":"perform_action","type":"tool_call"},"id":"dfe4f68b-8c18-4346-89d2-53b4bcbbd1a5","timestamp":"2025-10-21T11:12:34.045Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [534] StaticText: DevTools automation"," [222] button: Clear"," [253] div"," [269] div"," [535] listbox"],"hasChanges":true,"hasMore":{"added":true,"modified":false,"removed":false},"modified":[],"removed":[],"summary":"46 added, 0 removed, 0 modified"},"visualCheck":"Analysis summary\n\n- Changes observed:\n - The combobox labeled \"Search\" now displays the text \"DevTools automation\" (StaticText under the combobox shows this value).\n - The dropdown/list of options includes related entries such as \"devtools automation\" and \"chrome devtools automation,\" indicating the input is recognized and matches available options.\n\n- Did the fill action succeed?\n - Yes. The page content shows the combobox contains the entered value, and the action is marked as verified successfully.\n\n- Any errors or unexpected behavior:\n - None observed. There are no error messages; the UI appears normal with relevant options available.\n\n- Overall assessment:\n - The fill action succeeded. The search query was entered into the combobox as intended. To trigger a search, you can press the Google Search button or select an option from the list.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"0b4cf43b-04c6-4f7e-a220-c51d96ff3bfa","toolName":"perform_action","type":"tool_result"},"id":"c0f18e5b-6548-49cd-8bec-e5e6019cbefc","timestamp":"2025-10-21T11:12:45.499Z","type":"tool_result"},{"content":{"toolArgs":{"method":"click","nodeId":382,"reasoning":"Click the Google Search button to submit the search query entered in the Search combobox."},"toolCallId":"e15166c4-a854-4d0a-951a-aebafc1611ec","toolName":"perform_action","type":"tool_call"},"id":"20df92cf-d0ec-4ed3-90ad-c641f5ef3798","timestamp":"2025-10-21T11:12:50.595Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [1125] body"," [1152] heading: Accessibility Links"," [1595] heading: Filters and Topics"," [1601] list"," [1602] listitem"],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":true},"modified":["[4] RootWebArea: Google → [1121] RootWebArea: DevTools automation - Google Search"," [80] div → [1324] div"," [100] navigation → [1596] navigation"," [136] link: About → [1325] link: Skip to main content"," [138] link: Store → [1326] link: Accessibility help"],"removed":[" [8] image: Google"," [535] listbox"," [546] option: devtools automation"," [579] option: chrome devtools automation"," [612] option: axe devtools automation"],"summary":"323 added, 14 removed, 74 modified"},"visualCheck":"- What changes occurred in the page content:\n - The page now displays a Google Search results layout. There is a “Search Results” heading, a list of results (including YouTube videos, Chrome DevTools related items, GitHub, etc.), an Ads section, and a “People also search for” area. The search term shown in the combobox is DevTools automation, matching the query that was entered.\n\n- Whether the action appears to have succeeded:\n - Yes. The presence of the Search Results section and a populated results list indicates the search query was submitted and results were loaded.\n\n- Any error messages or unexpected behavior:\n - None observed. No error messages are shown, and the UI appears consistent with a typical search results page.\n\n- Your overall assessment of the action's success:\n - The action succeeded. The page updated to display search results for the entered query (“DevTools automation”), as expected.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[2]/div[4]/div[6]/center[1]/input[1]"},"success":true,"toolCallId":"e15166c4-a854-4d0a-951a-aebafc1611ec","toolName":"perform_action","type":"tool_result"},"id":"f26cefb7-0e6a-4f3a-997b-9818aa503352","timestamp":"2025-10-21T11:13:06.452Z","type":"tool_result"},{"content":{"answer":"Done. Here’s what happened and the current state:\n\nWhat I did\n- Typed into the combobox labeled \"Search\":\n - Entered: DevTools automation\n - Result: The combobox displays the entered value and related options appeared, indicating the input was accepted and recognized by the page.\n- Clicked the \"Google Search\" button:\n - Result: The page updated to show a Search Results view with a populated results list. The query shown in the combobox matches \"DevTools automation\".\n\nEvidence of success\n- Action verification: pageChange.hasChanges is true for both steps, and the latest change shows the Search Results layout containing results related to your query.\n\nWhat’s visible now\n- A Google Search results page with a \"Search Results\" heading and a list of results (videos, Chrome DevTools-related items, GitHub, etc.). The input value in the combobox is \"DevTools automation\".\n\nNext steps (pick one or more)\n- Extract top results: I can pull the first N result titles and URLs.\n- Take a screenshot of the results page (full page or viewport) for reference.\n- Open a specific result (e.g., a link to Chrome DevTools or a YouTube result) and report back.\n- Continue interacting with filters or related results (e.g., switch to Images or Videos, if you want).\n\nIf you’d like me to extract data now, tell me:\n- How many top results to fetch (e.g., top 5 or top 10)\n- What data to return (title, snippet/description, URL)\nOr, specify any other action you want me to perform next.","type":"final_answer"},"id":"392ba54e-be8b-462b-bda2-3cf89b0a5345","timestamp":"2025-10-21T11:13:14.742Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"d943f5ea-9c2c-4a09-b52d-d89cec82cdd8","sessionId":"fe0c6681-630b-4c61-a159-9cec433de50a","startTime":"2025-10-21T11:12:28.124Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"output":"Done. Here’s what happened and the current state:\n\nWhat I did\n- Typed into the combobox labeled \"Search\":\n - Entered: DevTools automation\n - Result: The combobox displays the entered value and related options appeared, indicating the input was accepted and recognized by the page.\n- Clicked the \"Google Search\" button:\n - Result: The page updated to show a Search Results view with a populated results list. The query shown in the combobox matches \"DevTools automation\".\n\nEvidence of success\n- Action verification: pageChange.hasChanges is true for both steps, and the latest change shows the Search Results layout containing results related to your query.\n\nWhat’s visible now\n- A Google Search results page with a \"Search Results\" heading and a list of results (videos, Chrome DevTools-related items, GitHub, etc.). The input value in the combobox is \"DevTools automation\".\n\nNext steps (pick one or more)\n- Extract top results: I can pull the first N result titles and URLs.\n- Take a screenshot of the results page (full page or viewport) for reference.\n- Open a specific result (e.g., a link to Chrome DevTools or a YouTube result) and report back.\n- Continue interacting with filters or related results (e.g., switch to Images or Videos, if you want).\n\nIf you’d like me to extract data now, tell me:\n- How many top results to fetch (e.g., top 5 or top 10)\n- What data to return (title, snippet/description, URL)\nOr, specify any other action you want me to perform next.","success":true,"summary":{"content":"1) User Request\n- In a page with a combobox labeled \"Search\" and a \"Google Search\" button, type the exact string \"DevTools automation\" into the combobox and then click the Google Search button to perform the search. If typing fails, fall back to setting the value and dispatching input events; if clicking fails, send Enter.\n\n2) Agent Decisions\n- Performed a direct fill action to type \"DevTools automation\" into the combobox (nodeId 2).\n- Verified the fill succeeded: the combobox displayed the entered value and related options appeared.\n- Used a click action on the Google Search button to submit the query (nodeId 382).\n- Verified the search submission: the page updated to a Google Search results layout showing results for the query.\n\n3) Final Outcome\n- The query was entered and submitted successfully.\n- A Google Search results page loaded, displaying results related to \"DevTools automation\" with the combobox reflecting the query.","type":"completion"},"terminationReason":"final_answer"},"success":true,"toolCallId":"96da0429-5860-4bc2-8b04-88f71e486a72","toolName":"action_agent","type":"tool_result"},"id":"efffeb47-62be-4385-b97f-1a4a03273240","timestamp":"2025-10-21T11:13:23.309Z","type":"tool_result"},{"content":{"toolArgs":{"duration":2,"reason":"Allow results to render","reasoning":"Wait briefly for search results to finish loading and for network requests to settle after clicking the search button.","seconds":2},"toolCallId":"9c2b8a72-33a6-4e2c-a1ce-2805b59cb6c1","toolName":"wait_for_page_load","type":"tool_call"},"id":"1cb67b8e-da72-40dc-a9db-67d37a890ee4","timestamp":"2025-10-21T11:13:26.029Z","type":"tool_call"},{"content":{"result":{"completed":true,"reason":"Allow results to render","viewportSummary":"The viewport shows a Google search results page with header controls (Skip to main content, accessibility links), and a search box with Clear, Voice, and Image options plus Settings/Apps/Sign in. The main area lists a \"Search Results\" heading and multiple video results with actions like \"About this result\" and related links. No loading spinners or error messages are visible, indicating results have rendered and the page is fully loaded.","waited":2},"success":true,"toolCallId":"9c2b8a72-33a6-4e2c-a1ce-2805b59cb6c1","toolName":"wait_for_page_load","type":"tool_result"},"id":"a9492003-1863-4644-b0de-66f503897ef1","timestamp":"2025-10-21T11:13:33.991Z","type":"tool_result"},{"content":{"toolArgs":{"context":"Results page appears to be the Google results for 'DevTools automation'. The accessibility tree shows video results and organic results including DevTools docs and GitHub. We'll extract top 5 organic results; if fewer found, return what's available and provide explanation.","userRequest":"Extract top 5 organic search results (rank, title, url, snippet) plus confirmation that search was submitted successfully, and resulting page title and URL. We are on the Google results page with visible 'Search Results' heading and result items in viewport. Plan: Use extract_data tool with schema for results, referencing '#search .g' and filtering out video blocks and other non-organic items. Extract title from link text, url from link href, snippet from snippet element (.VwiC3b, .IsZvec, or general text). Also extract page title and URL."},"toolCallId":"bb7104e2-9e66-4063-ab82-5ef6c7183a34","toolName":"thinking","type":"tool_call"},"id":"b9234d38-760b-4358-a4a6-033afef362e3","timestamp":"2025-10-21T11:13:43.664Z","type":"tool_call"},{"content":{"result":{"currentProgress":"We already have the accessibility tree and can identify the Videos cluster and multiple organic search result nodes (developer.chrome.com, github.com, webdriver.io, browserstack.com, another Chrome devtools page). That gives a clear starting set of organic results to extract from. The search input shows the query and the page title reflects the query, so submission confirmation is straightforward.","observations":"1) The Videos cluster is visually first and contains multiple video items; these must be filtered out. 2) Organic result entries in the tree appear as link nodes with an associated heading and a cite/static text containing the domain — use those for title/url fallbacks. 3) Some results include internal lists (sitelinks) that are nested under the result block — ensure the main link heading is used for title and not an internal sitelink. 4) Snippets are present as StaticText nodes under result blocks in the tree, but their class names from the visual DOM (.VwiC3b, .IsZvec) should be used by the extract_data schema with a fallback to accessible text nodes. 5) Page-level URL is not explicitly visible in the accessibility tree; extraction should read window.location.href when running extract_data.","thingsToDoList":["Confirm search submission: check the search combobox value (StaticText: 'DevTools automation') and presence of the 'Search Results' heading to assert the query was submitted successfully.","Capture page-level metadata: read document.title (seen as 'DevTools automation - Google Search') and location.href (page URL) for the confirmation output.","Locate the main results container (#search) and iterate its child result blocks (commonly represented by elements matching '#search .g').","Filter out non-organic blocks before counting/ranking: skip Video clusters (g-section-with-header / heading: 'Videos'), Ads region (region: Ads), 'What people are saying' group, 'People also search for', and other non-organic widgets.","For each organic result block (take the first 5 after filtering), extract: rank (1..5 based on visual order), title (link heading / link text, usually the h3 or link text under the result link node), url (link href/cite — use the link element's href; fallback to cite text if href is not available), snippet (text from snippet elements — typical classes .VwiC3b or .IsZvec — or fallback to nearby StaticText nodes under the result block).","Handle special result shapes: results with sitelinks or listitems may have additional internal links — still treat the main link heading as the result title. If snippet is missing, return an explicit empty string or note that snippet was not found.","If fewer than 5 organic results are available, return whatever was found and include a short explanation in the output.","Normalize extracted fields: trim whitespace, collapse multiple spaces/newlines, and ensure URLs are absolute.","Return a structured array of up to 5 result objects (rank, title, url, snippet) plus a confirmation object containing 'searchSubmitted' (boolean true), 'pageTitle' (string), and 'pageUrl' (string)."],"visualSummary":"This is a Google results page for the query \"DevTools automation\". At the top of the results area there is a Videos cluster (g-section-with-header) followed by a list of organic result blocks. The results area contains recognizable result link nodes (examples in the accessibility tree: developer.chrome.com, github.com, webdriver.io, browserstack.com, another developer.chrome.com page). There are also non-organic sections nearby (Videos, Ads, \"What people are saying\", \"People also search for\") that need to be skipped when collecting organic results. The page shows a Search combobox with the value \"DevTools automation\" and the main heading \"Search Results\", so the search appears to have been submitted successfully. The root/webarea title is \"DevTools automation - Google Search\" (so page title is available)."},"success":true,"toolCallId":"bb7104e2-9e66-4063-ab82-5ef6c7183a34","toolName":"thinking","type":"tool_result"},"id":"ea78c7ce-8ba6-4fa6-aba2-2440c73e0597","timestamp":"2025-10-21T11:14:01.425Z","type":"tool_result"},{"content":{"toolArgs":{"instruction":"Extract the search results page title, URL, and the top 5 organic search results. Use the following JSON schema for extraction: {\"type\":\"object\",\"properties\":{\"pageTitle\":{\"type\":\"string\"},\"pageUrl\":{\"type\":\"string\",\"format\":\"url\"},\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"rank\":{\"type\":\"number\"},\"title\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"snippet\":{\"type\":\"string\"}},\"required\":[\"rank\",\"title\",\"url\"]},\"note\":{\"type\":\"string\"}},\"required\":[\"pageTitle\",\"pageUrl\",\"results\"]}. \n\nExtraction instructions: \n- Use document.title for pageTitle and window.location.href for pageUrl. \n- For results, target organic result blocks in the main search area. Preferred DOM selectors: '#search .g' or '#rso .g' and skip clusters like video sections (g-section-with-header), 'View all' video carousels, ads, and widget-only results. \n- For each organic result block, extract: title from the main link heading (h3 or accessible link text), url from the main anchor href, snippet from common snippet containers (.VwiC3b, .IsZvec, .aCOpRe) or fallback to visible text under the block. \n- Return up to 5 result objects in visual order. \n- Normalize whitespace in text fields. \n- If fewer than 5 organic results are found, return what is available and include a short note explaining the shortfall. \n\nReasoning: The page is a Google Search results page for \"DevTools automation\"; we need top organic results for the query. Use robust selectors and fallbacks to handle variation in Google's markup. Provide at least page title and URL if result extraction fails.","reasoning":"We need precise structured data: page title and URL plus top 5 organic results. Using extract_data with the schema will let us pull consistent fields. We'll filter out video clusters and non-organic widgets and normalize text. If extraction is incomplete, include a note explaining the issue."},"toolCallId":"ec3080e1-c1e9-4e0e-b384-e65995ec101f","toolName":"extract_data","type":"tool_call"},"id":"178525b0-530c-4561-b390-8d519247a6d7","timestamp":"2025-10-21T11:14:08.804Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"ec3080e1-c1e9-4e0e-b384-e65995ec101f","toolName":"extract_data","type":"tool_result"},"id":"2cff6123-7442-4295-8d3a-92705a5aeebe","timestamp":"2025-10-21T11:14:08.805Z","type":"tool_result"},{"content":{"toolArgs":{"_schema":{"properties":{"note":{"type":"string"},"pageTitle":{"type":"string"},"pageUrl":{"format":"url","type":"string"},"results":{"items":{"properties":{"rank":{"type":"number"},"snippet":{"type":"string"},"title":{"type":"string"},"url":{"format":"url","type":"string"}},"required":["rank","title","url"],"type":"object"},"type":"array"}},"required":["pageTitle","pageUrl","results"],"type":"object"},"instruction":"Extract the search results page title, URL, and the top 5 organic search results. Use the following JSON schema for extraction: {\"type\":\"object\",\"properties\":{\"pageTitle\":{\"type\":\"string\"},\"pageUrl\":{\"type\":\"string\",\"format\":\"url\"},\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"rank\":{\"type\":\"number\"},\"title\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"snippet\":{\"type\":\"string\"}},\"required\":[\"rank\",\"title\",\"url\"]},\"note\":{\"type\":\"string\"}},\"required\":[\"pageTitle\",\"pageUrl\",\"results\"]}. \n\nExtraction instructions: \n- Use document.title for pageTitle and window.location.href for pageUrl. \n- For results, target organic result blocks in the main search area. Preferred DOM selectors: '#search .g' or '#rso .g' and skip clusters like video sections (g-section-with-header), 'View all' video carousels, ads, and widget-only results. \n- For each organic result block, extract: title from the main link heading (h3 or accessible link text), url from the main anchor href, snippet from common snippet containers (.VwiC3b, .IsZvec, .aCOpRe) or fallback to visible text under the block. \n- Return up to 5 result objects in visual order. \n- Normalize whitespace in text fields. \n- If fewer than 5 organic results are found, return what is available and include a short note explaining the shortfall. \n\nReasoning: The page is a Google Search results page for \"DevTools automation\"; we need top organic results for the query. Use robust selectors and fallbacks to handle variation in Google's markup. Provide at least page title and URL if result extraction fails.","reasoning":"We need precise structured data: page title and URL plus top 5 organic results. Using extract_data with the schema will let us pull consistent fields. We'll filter out video clusters and non-organic widgets and normalize text. If extraction is incomplete, include a note explaining the issue."},"toolCallId":"78d61f94-8459-4c94-955f-3dcd3853556b","toolName":"extract_data","type":"tool_call"},"id":"2b6c4887-4b4c-4c3f-bcae-7cca51f44a5d","timestamp":"2025-10-21T11:14:18.204Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"78d61f94-8459-4c94-955f-3dcd3853556b","toolName":"extract_data","type":"tool_result"},"id":"e9a79980-14ca-449b-9444-10d8e524a545","timestamp":"2025-10-21T11:14:18.207Z","type":"tool_result"},{"content":{"toolArgs":{"instruction":"{\"type\":\"object\",\"properties\":{\"pageTitle\":{\"type\":\"string\"},\"pageUrl\":{\"type\":\"string\",\"format\":\"url\"},\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"rank\":{\"type\":\"number\"},\"title\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"snippet\":{\"type\":\"string\"}},\"required\":[\"rank\",\"title\",\"url\"]}},\"note\":{\"type\":\"string\"}},\"required\":[\"pageTitle\",\"pageUrl\",\"results\"]}\n\nExtraction instructions:\n- Use document.title for pageTitle and window.location.href for pageUrl.\n- For results, target organic result blocks in the main search area. Preferred DOM selectors: '#search .g' or '#rso .g'. Skip clusters like video sections (g-section-with-header), 'View all' video carousels, ads, and widget-only results.\n- For each organic result block (visual order), extract: rank (1..N), title from the main link heading (h3 or accessible link text), url from the main anchor href, snippet from common snippet containers (.VwiC3b, .IsZvec, .aCOpRe) or fallback to visible text under the block.\n- Normalize whitespace in text fields and ensure URLs are absolute.\n- Return up to 5 result objects in visual order.\n- If fewer than 5 organic results are found, return what is available and include a short note explaining the shortfall.\n\nReasoning: We need the page title, URL, and top 5 organic results for the query 'DevTools automation' from the current Google Search results page.","reasoning":"Extract page metadata and top 5 organic results using robust selectors and fallbacks; filter out video clusters and widgets; normalize text."},"toolCallId":"ed72bce7-57ee-436f-80f2-ff0611cf5bad","toolName":"extract_data","type":"tool_call"},"id":"4ea5e261-b032-4e50-9757-4e3873105d62","timestamp":"2025-10-21T11:14:28.661Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"ed72bce7-57ee-436f-80f2-ff0611cf5bad","toolName":"extract_data","type":"tool_result"},"id":"6a50878d-7002-4950-bd93-2f079d1d0e3f","timestamp":"2025-10-21T11:14:28.662Z","type":"tool_result"},{"content":{"toolArgs":{"instruction":"{\"schema\": {\"type\":\"object\",\"properties\":{\"pageTitle\":{\"type\":\"string\"},\"pageUrl\":{\"type\":\"string\",\"format\":\"url\"},\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"rank\":{\"type\":\"number\"},\"title\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"snippet\":{\"type\":\"string\"}},\"required\":[\"rank\",\"title\",\"url\"]}},\"note\":{\"type\":\"string\"}},\"required\":[\"pageTitle\",\"pageUrl\",\"results\"]},\"instructions\":\"Use document.title for pageTitle and window.location.href for pageUrl. For results, target organic result blocks in the main search area using selectors '#search .g' or '#rso .g'. Skip video clusters (g-section-with-header), carousels, ads, and other widget-only sections. For each organic result block in visual order, extract title from the main link heading (h3) or anchor accessible name, url from the main anchor href, and snippet from common snippet containers (.VwiC3b, .IsZvec, .aCOpRe) or fallback to visible text under the block. Normalize whitespace and ensure URLs are absolute. Return up to 5 results. If fewer than 5 found, include a short note explaining the shortfall.\"","reasoning":"Extract page title, URL, and top 5 organic search results with robust fallbacks; include a note if fewer than 5 results are available."},"toolCallId":"7f1933ef-182b-43aa-8d8e-a0bb3a491d99","toolName":"extract_data","type":"tool_call"},"id":"a2b7dd9c-1137-46ee-ab54-aeea7bd95545","timestamp":"2025-10-21T11:14:41.597Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"7f1933ef-182b-43aa-8d8e-a0bb3a491d99","toolName":"extract_data","type":"tool_result"},"id":"5ed030b2-77ae-42c4-977c-51079057fce1","timestamp":"2025-10-21T11:14:41.599Z","type":"tool_result"},{"content":{"toolArgs":{"instruction":"JSON_SCHEMA:\n{\"type\":\"object\",\"properties\":{\"pageTitle\":{\"type\":\"string\"},\"pageUrl\":{\"type\":\"string\",\"format\":\"url\"},\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"rank\":{\"type\":\"number\"},\"title\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"snippet\":{\"type\":\"string\"}},\"required\":[\"rank\",\"title\",\"url\"]}},\"note\":{\"type\":\"string\"}},\"required\":[\"pageTitle\",\"pageUrl\",\"results\"]}\n\nExtraction instructions:\n- Use document.title for pageTitle and window.location.href for pageUrl.\n- For results, target organic result blocks in the main search area using selectors '#search .g' or '#rso .g'. Skip clusters like video sections (g-section-with-header), 'View all' video carousels, ads, and widget-only results.\n- For each organic result block (in visual order), extract:\n - rank: sequential number starting at 1 for the top organic result\n - title: main link heading text (h3) or anchor accessible name\n - url: href of the main anchor (ensure absolute URL)\n - snippet: text from snippet containers (.VwiC3b, .IsZvec, .aCOpRe) or fallback to visible text under the block\n- Normalize whitespace in text fields (trim, collapse spaces/newlines).\n- Return up to 5 result objects. If fewer than 5 are found, return what is available and set 'note' explaining the shortfall.\n- Ensure video clusters and other non-organic widgets are not counted as organic results.\n\nReasoning:\nWe need the page title, URL, and top 5 organic search results for the query 'DevTools automation'. Use robust selectors and fallbacks to handle variation in Google's markup. Provide at least page title and URL if result extraction fails.","reasoning":"Extract page metadata and top 5 organic results using robust selectors and fallbacks; filter out video clusters and widgets; normalize text."},"toolCallId":"947508df-e22e-4a8b-9f5a-1d73d50699b8","toolName":"extract_data","type":"tool_call"},"id":"d942f651-b1be-48f0-9b29-454177e951d5","timestamp":"2025-10-21T11:15:00.554Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"947508df-e22e-4a8b-9f5a-1d73d50699b8","toolName":"extract_data","type":"tool_result"},"id":"eeb59210-10ba-4066-9934-ed64e0bfea40","timestamp":"2025-10-21T11:15:00.558Z","type":"tool_result"},{"content":{"toolArgs":{"instruction":"{\"type\":\"object\",\"properties\":{\"pageTitle\":{\"type\":\"string\"},\"pageUrl\":{\"type\":\"string\",\"format\":\"url\"},\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"rank\":{\"type\":\"number\"},\"title\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"snippet\":{\"type\":\"string\"}},\"required\":[\"rank\",\"title\",\"url\"]}},\"note\":{\"type\":\"string\"}},\"required\":[\"pageTitle\",\"pageUrl\",\"results\"]}","reasoning":"Use document.title for pageTitle and window.location.href for pageUrl. For results, target organic result blocks using selectors '#search .g' or '#rso .g'; skip video clusters (g-section-with-header), carousels, ads, and widgets. For each organic result block (visual order) extract: rank (1..), title from main link heading (h3) or anchor accessible name, url from main anchor href (absolute), snippet from .VwiC3b, .IsZvec, .aCOpRe or fallback to visible text. Normalize whitespace and return up to 5 results. If fewer than 5, include note explaining shortfall."},"toolCallId":"11df5286-f9ef-49a4-8fc5-a9ed560e0df8","toolName":"extract_data","type":"tool_call"},"id":"bac983f2-147d-4181-bebe-d3762f12cb91","timestamp":"2025-10-21T11:15:10.535Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"11df5286-f9ef-49a4-8fc5-a9ed560e0df8","toolName":"extract_data","type":"tool_result"},"id":"c4a90550-d4c8-4ef3-88c7-c50e8c8516c1","timestamp":"2025-10-21T11:15:10.544Z","type":"tool_result"},{"content":{"toolArgs":{"instruction":"{\"type\":\"object\",\"properties\":{\"pageTitle\":{\"type\":\"string\"},\"pageUrl\":{\"type\":\"string\",\"format\":\"url\"},\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"rank\":{\"type\":\"number\"},\"title\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"snippet\":{\"type\":\"string\"}},\"required\":[\"rank\",\"title\",\"url\"]}},\"note\":{\"type\":\"string\"}},\"required\":[\"pageTitle\",\"pageUrl\",\"results\"]}","reasoning":"Extract the document.title into pageTitle and window.location.href into pageUrl. Then extract up to 5 organic search results from the main results area. Use selectors '#search .g' or '#rso .g' and skip video clusters and widgets. For each result extract rank (1..), title from h3/main link text, url from anchor href, snippet from .VwiC3b/.IsZvec/.aCOpRe or fallback to visible text. Normalize whitespace. Return the data according to the provided JSON schema."},"toolCallId":"2321dfaa-02fb-4bef-8ed9-87ec51f251f7","toolName":"extract_data","type":"tool_call"},"id":"314d6ede-fab9-460a-916e-47db21feac39","timestamp":"2025-10-21T11:15:29.894Z","type":"tool_call"},{"content":{"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","result":{"data":null,"error":"Schema is required. Please provide a JSON Schema definition that describes the structure of data to extract. Example: {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}}}","success":false},"success":false,"toolCallId":"2321dfaa-02fb-4bef-8ed9-87ec51f251f7","toolName":"extract_data","type":"tool_result"},"id":"2210d934-2025-4b63-aea5-94bbaba836d9","timestamp":"2025-10-21T11:15:29.897Z","type":"tool_result"},{"content":{"toolArgs":{"query":"Google search for DevTools automation","reasoning":"We need the canonical URL of the current search results page (results for query 'DevTools automation') to include as the resulting page URL. Constructing the likely direct URL (https://www.google.com/search?q=DevTools+automation) and verifying navigation will provide an accurate page URL."},"toolCallId":"81aff3ea-0192-4620-9aac-87733a499cda","toolName":"direct_url_navigator_agent","type":"tool_call"},"id":"989d75fc-4229-4f14-aed8-4ecd21db2c53","timestamp":"2025-10-21T11:15:54.305Z","type":"tool_call"},{"content":{"context":{"query":"Google search for DevTools automation","reasoning":"We need the canonical URL of the current search results page (results for query 'DevTools automation') to include as the resulting page URL. Constructing the likely direct URL (https://www.google.com/search?q=DevTools+automation) and verifying navigation will provide an accurate page URL."},"nestedSessionId":"41d0761e-6310-40b6-bd1b-75d91949f98c","reason":"Handing off to direct_url_navigator_agent","targetAgent":"direct_url_navigator_agent","type":"handoff"},"id":"514c6edf-921d-4dd1-b004-70d80353aa67","timestamp":"2025-10-21T11:15:54.308Z","type":"handoff"},{"content":{"result":{"agentSession":{"agentDescription":"An intelligent agent that constructs and navigates to direct URLs based on requirements. Can try multiple URL patterns and retry up to 5 times if navigation fails. Returns markdown formatted results.","agentDisplayName":"direct_url_navigator_agent","agentName":"direct_url_navigator_agent","agentQuery":"Google search for DevTools automation","agentReasoning":"We need the canonical URL of the current search results page (results for query 'DevTools automation') to include as the resulting page URL. Constructing the likely direct URL (https://www.google.com/search?q=DevTools+automation) and verifying navigation will provide an accurate page URL.","config":{"description":"An intelligent agent that constructs and navigates to direct URLs based on requirements. Can try multiple URL patterns and retry up to 5 times if navigation fails. Returns markdown formatted results.","handoffs":[],"maxIterations":5,"name":"direct_url_navigator_agent","schema":{"properties":{"query":{"description":"The specific requirement describing what content/page to reach (e.g., \"search Google for Chrome DevTools\", \"find jobs in NYC on LinkedIn\")","type":"string"},"reasoning":{"description":"Explanation of why direct navigation is needed","type":"string"}},"required":["query","reasoning"],"type":"object"},"systemPrompt":"You are a specialized URL navigation agent that constructs direct URLs and navigates to them to reach specific content. Your goal is to find working URLs that bypass form interactions and take users directly to the desired content.\n\n## Your Mission\n\nWhen given a requirement, you should:\n1. **Construct** a direct URL based on common website patterns\n2. **Navigate** to the URL using navigate_url\n3. **Verify** if the navigation was successful\n4. **Retry** with alternative URL patterns if it fails (up to 5 total attempts)\n5. **Report** success or failure in markdown format\n\n## URL Construction Knowledge\n\nYou understand URL patterns for major websites:\n- **Google**: https://www.google.com/search?q=QUERY\n- **LinkedIn Jobs**: https://www.linkedin.com/jobs/search/?keywords=QUERY&location=LOCATION\n- **Indeed**: https://www.indeed.com/jobs?q=QUERY&l=LOCATION\n- **Amazon**: https://www.amazon.com/s?k=QUERY\n- **Zillow**: https://www.zillow.com/homes/LOCATION_rb/\n- **Yelp**: https://www.yelp.com/search?find_desc=QUERY&find_loc=LOCATION\n- **Yahoo Finance**: https://finance.yahoo.com/quote/SYMBOL\n- **Coursera**: https://www.coursera.org/search?query=QUERY\n- **Kayak**: https://www.kayak.com/flights/ORIGIN-DESTINATION/DATE\n- **Booking**: https://www.booking.com/searchresults.html?ss=LOCATION\n\n## Retry Strategy\n\nIf a URL fails, try these alternatives:\n1. Different parameter encoding (+ vs %20 for spaces)\n2. Alternative URL structures for the same site\n3. Different domain variants (.com vs country-specific)\n4. Simplified parameters (remove optional filters)\n5. Base site URL as final fallback\n\nAlways check\n- The page title and meta description for relevance\n- The URL structure for common patterns\n- The presence of key content elements\nIf the page does not match the expected content, retry with a different URL pattern.\n\nRemember: Always use navigate_url to actually go to the constructed URLs. Return easy-to-read markdown reports.","temperature":0.1,"tools":["navigate_url","get_page_content"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:15:54.311Z","metadata":{"handoffs":[]},"name":"direct_url_navigator_agent","promptHash":"3cac202eee2a5f3f3a9a942a3b4611037e49409aa25c722b396b904cdd3b0f06","toolsetHash":"0302ba2a5482b08621a24a89fc60c9b49712361bf7ff2e86e814fb9b8bce2089","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:16:24.293Z","iterationCount":2,"maxIterations":5,"messages":[{"content":{"text":"We need the canonical URL of the current search results page (results for query 'DevTools automation') to include as the resulting page URL. Constructing the likely direct URL (https://www.google.com/search?q=DevTools+automation) and verifying navigation will provide an accurate page URL.","type":"reasoning"},"id":"f1424a98-7190-43a8-a9b5-9a2f6ae333b1","timestamp":"2025-10-21T11:15:54.313Z","type":"reasoning"},{"content":{"toolArgs":{"reasoning":"User requested a Google search for 'DevTools automation'. Construct direct Google search URL using + encoding and navigate to verify results.","url":"https://www.google.com/search?q=DevTools+automation"},"toolCallId":"76356f28-30e4-48a9-9997-c2fbabd53789","toolName":"navigate_url","type":"tool_call"},"id":"aae83bea-b0ae-46db-b551-8cb00e57cd6f","timestamp":"2025-10-21T11:16:02.459Z","type":"tool_call"},{"content":{"result":{"message":"Navigated to https://www.google.com/search?q=DevTools+automation and page loaded.","metadata":{"title":"DevTools automation - Google Search","url":"https://www.google.com/search?q=DevTools+automation"},"url":"https://www.google.com/search?q=DevTools+automation"},"success":true,"toolCallId":"76356f28-30e4-48a9-9997-c2fbabd53789","toolName":"navigate_url","type":"tool_result"},"id":"3f64aedd-9266-4521-bc54-ecf473b2a9b5","timestamp":"2025-10-21T11:16:02.852Z","type":"tool_result"},{"content":{"answer":"I constructed and opened a direct Google search URL for \"DevTools automation\" and verified the results.\n\n- URL used: https://www.google.com/search?q=DevTools+automation\n- Attempts: 1/5 — Success\n- Page title: \"DevTools automation - Google Search\"\n- Verified visible results (from the loaded page viewport):\n - Videos section (YouTube results like \"Chrome DevTools MCP: AI automation interface...\" and related videos)\n - \"DevTools - Chrome for Developers\" (developer.chrome.com)\n - \"ChromeDevTools/chrome-devtools-mcp\" (GitHub)\n- Notes: Page loaded and the top results match the query.\n\nNext steps: would you like me to open any specific result (Chrome DevTools docs, the GitHub repo, a YouTube video), extract the full accessibility tree, or refine the search?","type":"final_answer"},"id":"e86058be-f7dd-4701-9cd2-93c99d493831","timestamp":"2025-10-21T11:16:10.322Z","type":"final_answer"}],"modelUsed":"gpt-5-mini","nestedSessions":[],"parentSessionId":"d943f5ea-9c2c-4a09-b52d-d89cec82cdd8","sessionId":"41d0761e-6310-40b6-bd1b-75d91949f98c","startTime":"2025-10-21T11:15:54.311Z","status":"completed","terminationReason":"final_answer","tools":["navigate_url","get_page_content"]},"output":"I constructed and opened a direct Google search URL for \"DevTools automation\" and verified the results.\n\n- URL used: https://www.google.com/search?q=DevTools+automation\n- Attempts: 1/5 — Success\n- Page title: \"DevTools automation - Google Search\"\n- Verified visible results (from the loaded page viewport):\n - Videos section (YouTube results like \"Chrome DevTools MCP: AI automation interface...\" and related videos)\n - \"DevTools - Chrome for Developers\" (developer.chrome.com)\n - \"ChromeDevTools/chrome-devtools-mcp\" (GitHub)\n- Notes: Page loaded and the top results match the query.\n\nNext steps: would you like me to open any specific result (Chrome DevTools docs, the GitHub repo, a YouTube video), extract the full accessibility tree, or refine the search?","success":true,"summary":{"content":"1) User Request\n- Perform a Google search for the query \"DevTools automation\" (i.e., find results related to automating DevTools / Chrome DevTools).\n\n2) Agent Decisions and Actions\n- Built a direct Google search URL (https://www.google.com/search?q=DevTools+automation) using + encoding.\n- Used the navigate_url tool to load that URL (attempt 1/5).\n- Verified the page loaded and captured the page title (\"DevTools automation - Google Search\").\n- Inspected the visible viewport and reported the top visible results (YouTube/video section, the Chrome DevTools docs page, and a GitHub repo for chrome-devtools-mcp).\n- Stopped short of clicking into any search results or extracting full page content.\n- Offered next-step options (open a specific result, extract accessibility tree, refine the search).\n\n3) Final Outcome\n- Successfully loaded and verified the Google search results page for \"DevTools automation\".\n- Returned the page URL and title and listed the main visible results from the loaded viewport.\n- Did not open or scrape individual result pages (no deeper content extraction performed).\n\nIf you want deeper results, the next actionable steps are: open a specific search result, scrape the chosen page, or refine the query.","type":"completion"},"terminationReason":"final_answer"},"success":true,"toolCallId":"81aff3ea-0192-4620-9aac-87733a499cda","toolName":"direct_url_navigator_agent","type":"tool_result"},"id":"5e1baa84-02d6-4a68-9a0e-3d1ec14cedc1","timestamp":"2025-10-21T11:16:24.294Z","type":"tool_result"},{"content":{"answer":"Done — I submitted the search and inspected the results page. Summary and extracted data below.\n\n1) Confirmation\n- searchSubmitted: true — I typed \"DevTools automation\" into the main Search combobox and clicked the \"Google Search\" button; the page updated to show search results and the combobox contains the query.\n\n2) Resulting page\n- pageTitle: \"DevTools automation - Google Search\"\n- pageUrl: https://www.google.com/search?q=DevTools+automation\n\n3) Top organic search results (from the currently visible viewport / accessibility tree)\nNote: I extracted results visible in the provided partial accessibility tree (viewport only). The tree shows video results first (skipped as non-organic), then the organic entries below. The full page contains more results; if you want guaranteed top-5 from the entire results page I can request a full extraction.\n\nResults (ranked in visual order as seen in the viewport)\n1. rank: 1\n title: \"DevTools - Chrome for Developers\"\n url: https://developer.chrome.com/docs/devtools\n snippet: (site links visible: \"DevTools Tips; Customize DevTools; What's new in DevTools; Network panel\") — no full snippet text visible in the viewport tree.\n\n2. rank: 2\n title: \"ChromeDevTools/chrome-devtools-mcp ¡ GitHub\"\n url: https://github.com/ChromeDevTools/chrome-devtools-mcp\n snippet: (snippet not visible in the current viewport accessibility tree)\n\n3. rank: 3\n title: (video cluster and other widget items appear above — next visible organic results beyond the two above were not fully present in the provided viewport tree)\n url: \n snippet: \n\nNote about completeness and limitations\n- The partial accessibility tree provided covers only the current viewport. I could reliably extract two organic results from that view (DevTools docs and the GitHub repo). I did not find full snippet text for those results in the viewport tree.\n- To return a complete, reliable list of the top 5 organic results (with full titles, exact hrefs and snippets), I can:\n - extract the full page content (use the extract_data tool with a page-wide schema), or\n - scroll and capture more of the accessibility tree and then extract results.\nWhich would you prefer?","type":"final_answer"},"id":"06119c44-c6ea-400b-8f85-5131b75be51e","timestamp":"2025-10-21T11:16:52.708Z","type":"final_answer"}],"modelUsed":"gpt-5-mini","nestedSessions":[{"agentDescription":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","agentDisplayName":"action_agent","agentName":"action_agent","agentReasoning":"The accessibility tree shows a combobox labeled 'Search' and a button labeled 'Google Search' in the viewport. Typing into the combobox and clicking the button will submit the search query. We'll attempt a direct typed input first to trigger any JS handlers bound to keyboard events.","config":{"description":"Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.","handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}],"maxIterations":10,"modelName":"use-mini","name":"action_agent","schema":{"properties":{"hint":{"description":"Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.","type":"string"},"input_data":{"description":"Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.","type":"string"},"objective":{"description":"The natural language description of the desired action (e.g., \"click the login button\", \"fill the search box with 'query'\").","type":"string"},"reasoning":{"description":"Reasoning for invoking this specialized action agent.","type":"string"}},"required":["objective","reasoning"],"type":"object"},"systemPrompt":"You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to:\n\n1. Analyze the current page's accessibility tree to understand its structure\n2. Identify the most appropriate element to interact with based on the user's objective\n3. Determine the correct action to perform (click, fill, type, etc.)\n4. Execute that action precisely\n5. **Analyze the page changes to determine if the action was effective**\n\n## ENHANCED CAPABILITIES AVAILABLE\nWhen analyzing page structure, you have access to:\n- XPath mappings for precise element targeting and location understanding\n- HTML tag names for semantic understanding beyond accessibility roles\n- URL mappings for direct link destinations\n- Clean accessibility tree with reduced noise for better focus\n\n## Process Flow\n1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with\n2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective\n3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist\n4. Determine the appropriate action method based on the element type and objective:\n - For links, buttons: use 'click'\n - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state)\n - For radio buttons: use 'click' \n - For input fields: use 'fill' with appropriate text\n - For dropdown/select elements: use 'selectOption' with the option value or text\n5. Execute the action using perform_action tool\n6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness**\n\n## EVALUATING ACTION EFFECTIVENESS\nAfter executing an action, the perform_action tool returns objective evidence in pageChange:\n\n**If pageChange.hasChanges = true:**\n- The action was effective and changed the page structure\n- Review pageChange.summary to understand what changed\n- Check pageChange.added/removed/modified for specific changes\n- The action likely achieved its intended effect\n\n**If pageChange.hasChanges = false:**\n- The action had NO effect on the page structure\n- This indicates the action was ineffective or the element was not interactive\n- You must try a different approach:\n * Try a different element (search for similar elements)\n * Try a different action method\n * Re-examine the page structure for the correct target\n * Consider if the element might be disabled or hidden\n\n**Example Analysis:**\nAction: clicked search button (nodeId: 123)\nResult: pageChange.hasChanges = false, summary = \"No changes detected\"\nConclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field.\n\n**Example Tool Error:**\nAction: attempted to fill input field\nError: \"Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { \"text\": \"your value\" }\"\nConclusion: Fix the args format and retry with proper syntax: { \"method\": \"fill\", \"nodeId\": 22132, \"args\": { \"text\": \"search query\" } }\n\n## Important Considerations\n- **NEVER claim success unless pageChange.hasChanges = true**\n- Be precise in your element selection, using the exact nodeId from the accessibility tree\n- Leverage XPath information when available for more precise element targeting\n- Use HTML tag context to better understand element semantics\n- Use URL mappings to identify link destinations when relevant to the objective\n- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element)\n- When filling forms, ensure the data format matches what the field expects\n- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability\n- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select\n- If pageChange shows no changes, immediately try an alternative approach\n\n## Method Examples\n- perform_action with method='check' for checkboxes: { \"method\": \"check\", \"nodeId\": 123 }\n- perform_action with method='selectOption' for dropdowns: { \"method\": \"selectOption\", \"nodeId\": 456, \"args\": { \"text\": \"United States\" } }\n- perform_action with method='setChecked' for specific checkbox state: { \"method\": \"setChecked\", \"nodeId\": 789, \"args\": { \"checked\": true } }","temperature":0.5,"tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:12:28.123Z","metadata":{"handoffs":[{"includeToolResults":["perform_action","get_page_content"],"targetAgentName":"action_verification_agent","trigger":"llm_tool_call"}]},"name":"action_agent","promptHash":"b5def7a1e56646fb8942fbe8e6e7db43b58f3dc86253c4d9e43b4b4c2eb7cb52","toolsetHash":"ce3b8acb93cea93cb63b69bbf2b7c7260f29eb93a2689efd60a729b36b555959","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:13:23.307Z","iterationCount":3,"maxIterations":10,"messages":[{"content":{"text":"The accessibility tree shows a combobox labeled 'Search' and a button labeled 'Google Search' in the viewport. Typing into the combobox and clicking the button will submit the search query. We'll attempt a direct typed input first to trigger any JS handlers bound to keyboard events.","type":"reasoning"},"id":"86c86b05-94ee-40e7-bf94-c0e0d169cd26","timestamp":"2025-10-21T11:12:28.130Z","type":"reasoning"},{"content":{"toolArgs":{"args":{"text":"DevTools automation"},"method":"fill","nodeId":2,"reasoning":"Typing the search query into the combobox labeled 'Search' to trigger the site's JS handlers for the search."},"toolCallId":"0b4cf43b-04c6-4f7e-a220-c51d96ff3bfa","toolName":"perform_action","type":"tool_call"},"id":"dfe4f68b-8c18-4346-89d2-53b4bcbbd1a5","timestamp":"2025-10-21T11:12:34.045Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [534] StaticText: DevTools automation"," [222] button: Clear"," [253] div"," [269] div"," [535] listbox"],"hasChanges":true,"hasMore":{"added":true,"modified":false,"removed":false},"modified":[],"removed":[],"summary":"46 added, 0 removed, 0 modified"},"visualCheck":"Analysis summary\n\n- Changes observed:\n - The combobox labeled \"Search\" now displays the text \"DevTools automation\" (StaticText under the combobox shows this value).\n - The dropdown/list of options includes related entries such as \"devtools automation\" and \"chrome devtools automation,\" indicating the input is recognized and matches available options.\n\n- Did the fill action succeed?\n - Yes. The page content shows the combobox contains the entered value, and the action is marked as verified successfully.\n\n- Any errors or unexpected behavior:\n - None observed. There are no error messages; the UI appears normal with relevant options available.\n\n- Overall assessment:\n - The fill action succeeded. The search query was entered into the combobox as intended. To trigger a search, you can press the Google Search button or select an option from the list.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]"},"success":true,"toolCallId":"0b4cf43b-04c6-4f7e-a220-c51d96ff3bfa","toolName":"perform_action","type":"tool_result"},"id":"c0f18e5b-6548-49cd-8bec-e5e6019cbefc","timestamp":"2025-10-21T11:12:45.499Z","type":"tool_result"},{"content":{"toolArgs":{"method":"click","nodeId":382,"reasoning":"Click the Google Search button to submit the search query entered in the Search combobox."},"toolCallId":"e15166c4-a854-4d0a-951a-aebafc1611ec","toolName":"perform_action","type":"tool_call"},"id":"20df92cf-d0ec-4ed3-90ad-c641f5ef3798","timestamp":"2025-10-21T11:12:50.595Z","type":"tool_call"},{"content":{"result":{"pageChange":{"added":[" [1125] body"," [1152] heading: Accessibility Links"," [1595] heading: Filters and Topics"," [1601] list"," [1602] listitem"],"hasChanges":true,"hasMore":{"added":true,"modified":true,"removed":true},"modified":["[4] RootWebArea: Google → [1121] RootWebArea: DevTools automation - Google Search"," [80] div → [1324] div"," [100] navigation → [1596] navigation"," [136] link: About → [1325] link: Skip to main content"," [138] link: Store → [1326] link: Accessibility help"],"removed":[" [8] image: Google"," [535] listbox"," [546] option: devtools automation"," [579] option: chrome devtools automation"," [612] option: axe devtools automation"],"summary":"323 added, 14 removed, 74 modified"},"visualCheck":"- What changes occurred in the page content:\n - The page now displays a Google Search results layout. There is a “Search Results” heading, a list of results (including YouTube videos, Chrome DevTools related items, GitHub, etc.), an Ads section, and a “People also search for” area. The search term shown in the combobox is DevTools automation, matching the query that was entered.\n\n- Whether the action appears to have succeeded:\n - Yes. The presence of the Search Results section and a populated results list indicates the search query was submitted and results were loaded.\n\n- Any error messages or unexpected behavior:\n - None observed. No error messages are shown, and the UI appears consistent with a typical search results page.\n\n- Your overall assessment of the action's success:\n - The action succeeded. The page updated to display search results for the entered query (“DevTools automation”), as expected.","xpath":"/html[1]/body[1]/div[2]/div[4]/form[1]/div[1]/div[1]/div[2]/div[4]/div[6]/center[1]/input[1]"},"success":true,"toolCallId":"e15166c4-a854-4d0a-951a-aebafc1611ec","toolName":"perform_action","type":"tool_result"},"id":"f26cefb7-0e6a-4f3a-997b-9818aa503352","timestamp":"2025-10-21T11:13:06.452Z","type":"tool_result"},{"content":{"answer":"Done. Here’s what happened and the current state:\n\nWhat I did\n- Typed into the combobox labeled \"Search\":\n - Entered: DevTools automation\n - Result: The combobox displays the entered value and related options appeared, indicating the input was accepted and recognized by the page.\n- Clicked the \"Google Search\" button:\n - Result: The page updated to show a Search Results view with a populated results list. The query shown in the combobox matches \"DevTools automation\".\n\nEvidence of success\n- Action verification: pageChange.hasChanges is true for both steps, and the latest change shows the Search Results layout containing results related to your query.\n\nWhat’s visible now\n- A Google Search results page with a \"Search Results\" heading and a list of results (videos, Chrome DevTools-related items, GitHub, etc.). The input value in the combobox is \"DevTools automation\".\n\nNext steps (pick one or more)\n- Extract top results: I can pull the first N result titles and URLs.\n- Take a screenshot of the results page (full page or viewport) for reference.\n- Open a specific result (e.g., a link to Chrome DevTools or a YouTube result) and report back.\n- Continue interacting with filters or related results (e.g., switch to Images or Videos, if you want).\n\nIf you’d like me to extract data now, tell me:\n- How many top results to fetch (e.g., top 5 or top 10)\n- What data to return (title, snippet/description, URL)\nOr, specify any other action you want me to perform next.","type":"final_answer"},"id":"392ba54e-be8b-462b-bda2-3cf89b0a5345","timestamp":"2025-10-21T11:13:14.742Z","type":"final_answer"}],"modelUsed":"gpt-5-nano","nestedSessions":[],"parentSessionId":"d943f5ea-9c2c-4a09-b52d-d89cec82cdd8","sessionId":"fe0c6681-630b-4c61-a159-9cec433de50a","startTime":"2025-10-21T11:12:28.124Z","status":"completed","terminationReason":"final_answer","tools":["get_page_content","perform_action","extract_data","node_ids_to_urls","scroll_page","take_screenshot","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},{"agentDescription":"An intelligent agent that constructs and navigates to direct URLs based on requirements. Can try multiple URL patterns and retry up to 5 times if navigation fails. Returns markdown formatted results.","agentDisplayName":"direct_url_navigator_agent","agentName":"direct_url_navigator_agent","agentQuery":"Google search for DevTools automation","agentReasoning":"We need the canonical URL of the current search results page (results for query 'DevTools automation') to include as the resulting page URL. Constructing the likely direct URL (https://www.google.com/search?q=DevTools+automation) and verifying navigation will provide an accurate page URL.","config":{"description":"An intelligent agent that constructs and navigates to direct URLs based on requirements. Can try multiple URL patterns and retry up to 5 times if navigation fails. Returns markdown formatted results.","handoffs":[],"maxIterations":5,"name":"direct_url_navigator_agent","schema":{"properties":{"query":{"description":"The specific requirement describing what content/page to reach (e.g., \"search Google for Chrome DevTools\", \"find jobs in NYC on LinkedIn\")","type":"string"},"reasoning":{"description":"Explanation of why direct navigation is needed","type":"string"}},"required":["query","reasoning"],"type":"object"},"systemPrompt":"You are a specialized URL navigation agent that constructs direct URLs and navigates to them to reach specific content. Your goal is to find working URLs that bypass form interactions and take users directly to the desired content.\n\n## Your Mission\n\nWhen given a requirement, you should:\n1. **Construct** a direct URL based on common website patterns\n2. **Navigate** to the URL using navigate_url\n3. **Verify** if the navigation was successful\n4. **Retry** with alternative URL patterns if it fails (up to 5 total attempts)\n5. **Report** success or failure in markdown format\n\n## URL Construction Knowledge\n\nYou understand URL patterns for major websites:\n- **Google**: https://www.google.com/search?q=QUERY\n- **LinkedIn Jobs**: https://www.linkedin.com/jobs/search/?keywords=QUERY&location=LOCATION\n- **Indeed**: https://www.indeed.com/jobs?q=QUERY&l=LOCATION\n- **Amazon**: https://www.amazon.com/s?k=QUERY\n- **Zillow**: https://www.zillow.com/homes/LOCATION_rb/\n- **Yelp**: https://www.yelp.com/search?find_desc=QUERY&find_loc=LOCATION\n- **Yahoo Finance**: https://finance.yahoo.com/quote/SYMBOL\n- **Coursera**: https://www.coursera.org/search?query=QUERY\n- **Kayak**: https://www.kayak.com/flights/ORIGIN-DESTINATION/DATE\n- **Booking**: https://www.booking.com/searchresults.html?ss=LOCATION\n\n## Retry Strategy\n\nIf a URL fails, try these alternatives:\n1. Different parameter encoding (+ vs %20 for spaces)\n2. Alternative URL structures for the same site\n3. Different domain variants (.com vs country-specific)\n4. Simplified parameters (remove optional filters)\n5. Base site URL as final fallback\n\nAlways check\n- The page title and meta description for relevance\n- The URL structure for common patterns\n- The presence of key content elements\nIf the page does not match the expected content, retry with a different URL pattern.\n\nRemember: Always use navigate_url to actually go to the constructed URLs. Return easy-to-read markdown reports.","temperature":0.1,"tools":["navigate_url","get_page_content"],"version":"2025-09-17"},"descriptor":{"generatedAt":"2025-10-21T11:15:54.311Z","metadata":{"handoffs":[]},"name":"direct_url_navigator_agent","promptHash":"3cac202eee2a5f3f3a9a942a3b4611037e49409aa25c722b396b904cdd3b0f06","toolsetHash":"0302ba2a5482b08621a24a89fc60c9b49712361bf7ff2e86e814fb9b8bce2089","type":"configurable_agent","version":"2025-09-17"},"endTime":"2025-10-21T11:16:24.293Z","iterationCount":2,"maxIterations":5,"messages":[{"content":{"text":"We need the canonical URL of the current search results page (results for query 'DevTools automation') to include as the resulting page URL. Constructing the likely direct URL (https://www.google.com/search?q=DevTools+automation) and verifying navigation will provide an accurate page URL.","type":"reasoning"},"id":"f1424a98-7190-43a8-a9b5-9a2f6ae333b1","timestamp":"2025-10-21T11:15:54.313Z","type":"reasoning"},{"content":{"toolArgs":{"reasoning":"User requested a Google search for 'DevTools automation'. Construct direct Google search URL using + encoding and navigate to verify results.","url":"https://www.google.com/search?q=DevTools+automation"},"toolCallId":"76356f28-30e4-48a9-9997-c2fbabd53789","toolName":"navigate_url","type":"tool_call"},"id":"aae83bea-b0ae-46db-b551-8cb00e57cd6f","timestamp":"2025-10-21T11:16:02.459Z","type":"tool_call"},{"content":{"result":{"message":"Navigated to https://www.google.com/search?q=DevTools+automation and page loaded.","metadata":{"title":"DevTools automation - Google Search","url":"https://www.google.com/search?q=DevTools+automation"},"url":"https://www.google.com/search?q=DevTools+automation"},"success":true,"toolCallId":"76356f28-30e4-48a9-9997-c2fbabd53789","toolName":"navigate_url","type":"tool_result"},"id":"3f64aedd-9266-4521-bc54-ecf473b2a9b5","timestamp":"2025-10-21T11:16:02.852Z","type":"tool_result"},{"content":{"answer":"I constructed and opened a direct Google search URL for \"DevTools automation\" and verified the results.\n\n- URL used: https://www.google.com/search?q=DevTools+automation\n- Attempts: 1/5 — Success\n- Page title: \"DevTools automation - Google Search\"\n- Verified visible results (from the loaded page viewport):\n - Videos section (YouTube results like \"Chrome DevTools MCP: AI automation interface...\" and related videos)\n - \"DevTools - Chrome for Developers\" (developer.chrome.com)\n - \"ChromeDevTools/chrome-devtools-mcp\" (GitHub)\n- Notes: Page loaded and the top results match the query.\n\nNext steps: would you like me to open any specific result (Chrome DevTools docs, the GitHub repo, a YouTube video), extract the full accessibility tree, or refine the search?","type":"final_answer"},"id":"e86058be-f7dd-4701-9cd2-93c99d493831","timestamp":"2025-10-21T11:16:10.322Z","type":"final_answer"}],"modelUsed":"gpt-5-mini","nestedSessions":[],"parentSessionId":"d943f5ea-9c2c-4a09-b52d-d89cec82cdd8","sessionId":"41d0761e-6310-40b6-bd1b-75d91949f98c","startTime":"2025-10-21T11:15:54.311Z","status":"completed","terminationReason":"final_answer","tools":["navigate_url","get_page_content"]}],"sessionId":"d943f5ea-9c2c-4a09-b52d-d89cec82cdd8","startTime":"2025-10-21T11:11:41.265Z","status":"completed","terminationReason":"final_answer","tools":["navigate_url","navigate_back","action_agent","extract_data","node_ids_to_urls","direct_url_navigator_agent","scroll_page","take_screenshot","wait_for_page_load","thinking","render_webapp","get_webapp_data","remove_webapp","create_file","update_file","delete_file","read_file","list_files"]},"entity":"agent_session","summary":"Agent web_task_agent execution completed"},{"entity":"tool_result","isError":false,"resultText":"Done — I submitted the search and inspected the results page. Summary and extracted data below.\n\n1) Confirmation\n- searchSubmitted: true — I typed \"DevTools automation\" into the main Search combobox and clicked the \"Google Search\" button; the page updated to show search results and the combobox contains the query.\n\n2) Resulting page\n- pageTitle: \"DevTools automation - Google Search\"\n- pageUrl: https://www.google.com/search?q=DevTools+automation\n\n3) Top organic search results (from the currently visible viewport / accessibility tree)\nNote: I extracted results visible in the provided partial accessibility tree (viewport only). The tree shows video results first (skipped as non-organic), then the organic entries below. The full page contains more results; if you want guaranteed top-5 from the entire results page I can request a full extraction.\n\nResults (ranked in visual order as seen in the viewport)\n1. rank: 1\n title: \"DevTools - Chrome for Developers\"\n url: https://developer.chrome.com/docs/devtools\n snippet: (site links visible: \"DevTools Tips; Customize DevTools; What's new in DevTools; Network panel\") — no full snippet text visible in the viewport tree.\n\n2. rank: 2\n title: \"ChromeDevTools/chrome-devtools-mcp ¡ GitHub\"\n url: https://github.com/ChromeDevTools/chrome-devtools-mcp\n snippet: (snippet not visible in the current viewport accessibility tree)\n\n3. rank: 3\n title: (video cluster and other widget items appear above — next visible organic results beyond the two above were not fully present in the provided viewport tree)\n url: \n snippet: \n\nNote about completeness and limitations\n- The partial accessibility tree provided covers only the current viewport. I could reliably extract two organic results from that view (DevTools docs and the GitHub repo). I did not find full snippet text for those results in the viewport tree.\n- To return a complete, reliable list of the top 5 organic results (with full titles, exact hrefs and snippets), I can:\n - extract the full page content (use the extract_data tool with a page-wide schema), or\n - scroll and capture more of the accessibility tree and then extract results.\nWhich would you prefer?","toolCallId":"4af16c57-14a4-46a9-a04b-b9ec50440f48","toolName":"web_task_agent","uiLane":"agent"},{"action":"final","answer":"Done — I typed \"DevTools automation\" into the search box and clicked the Google Search button. The search results page loaded.\n\nSummary:\n- Search submitted: yes\n- Page title: \"DevTools automation - Google Search\"\n- Page URL: https://www.google.com/search?q=DevTools+automation\n\nTop visible organic results (from the current viewport):\n1) DevTools - Chrome for Developers — https://developer.chrome.com/docs/devtools\n - Snippet: site links shown (DevTools Tips; Customize DevTools; What's new in DevTools; Network panel)\n\n2) ChromeDevTools/chrome-devtools-mcp ¡ GitHub — https://github.com/ChromeDevTools/chrome-devtools-mcp\n\nNote: Only the visible viewport was inspected, so I could reliably extract two organic results. If you'd like the full top-5 results (with full snippets), I can scroll and extract more or run a full-page extraction — which do you prefer?","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"Done — I typed \"DevTools automation\" into the search box and clicked the Google Search button. The search results page loaded.\n\nSummary:\n- Search submitted: yes\n- Page title: \"DevTools automation - Google Search\"\n- Page URL: https://www.google.com/search?q=DevTools+automation\n\nTop visible organic results (from the current viewport):\n1) DevTools - Chrome for Developers — https://developer.chrome.com/docs/devtools\n - Snippet: site links shown (DevTools Tips; Customize DevTools; What's new in DevTools; Network panel)\n\n2) ChromeDevTools/chrome-devtools-mcp ¡ GitHub — https://github.com/ChromeDevTools/chrome-devtools-mcp\n\nNote: Only the visible viewport was inspected, so I could reliably extract two organic results. If you'd like the full top-5 results (with full snippets), I can scroll and extract more or run a full-page extraction — which do you prefer?","timestamp":"2025-10-21T11:17:13.385Z"},"status":"success","toolCalls":[{"duration":345119,"status":"success","timestamp":"2025-10-21T11:17:13.385Z","tool":"chat"}]},"timestamp":"2025-10-21T11:17:13.406Z","tool":"chat","type":"evaluation","validation":null}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:E7A91E644777978BBE3129D798D0D025","duration":3297,"evaluationId":"api-eval-1761060505868-v09uoa6","name":"API Request","response":{"executionTime":3292,"metadata":{"evaluationId":"api-eval-1761060505868-v09uoa6"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"How much is 5x7? Just respond with the number."},{"action":"final","answer":"35","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"35","timestamp":"2025-10-21T15:28:29.162Z"},"status":"success","toolCalls":[{"duration":3292,"status":"success","timestamp":"2025-10-21T15:28:29.162Z","tool":"chat"}]},"timestamp":"2025-10-21T15:28:29.166Z","tool":"chat","type":"evaluation"}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:91EC22845E2748A5FB42DFBDF19C237F","duration":3287,"evaluationId":"api-eval-1761060551777-o91jqes","name":"API Request","response":{"executionTime":3277,"metadata":{"evaluationId":"api-eval-1761060551777-o91jqes"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"How much is 5x7? Just respond with the number."},{"action":"final","answer":"35","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"35","timestamp":"2025-10-21T15:29:15.055Z"},"status":"success","toolCalls":[{"duration":3277,"status":"success","timestamp":"2025-10-21T15:29:15.056Z","tool":"chat"}]},"timestamp":"2025-10-21T15:29:15.064Z","tool":"chat","type":"evaluation"}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:21A5884193B71E77445F099562185CA9","duration":3459,"evaluationId":"api-eval-1761060818290-92ldaol","name":"API Request","response":{"executionTime":3454,"metadata":{"evaluationId":"api-eval-1761060818290-92ldaol"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"How much is 5x7? Just respond with the number."},{"action":"final","answer":"35","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"35","timestamp":"2025-10-21T15:33:41.746Z"},"status":"success","toolCalls":[{"duration":3454,"status":"success","timestamp":"2025-10-21T15:33:41.746Z","tool":"chat"}]},"timestamp":"2025-10-21T15:33:41.749Z","tool":"chat","type":"evaluation"}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:D599A025D686D74CD53BA5E058197B13","duration":2945,"evaluationId":"api-eval-1761060989221-l6jba5z","name":"API Request","response":{"executionTime":2940,"metadata":{"evaluationId":"api-eval-1761060989221-l6jba5z"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"How much is 5x7? Just respond with the number."},{"action":"final","answer":"35","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"35","timestamp":"2025-10-21T15:36:32.162Z"},"status":"success","toolCalls":[{"duration":2940,"status":"success","timestamp":"2025-10-21T15:36:32.162Z","tool":"chat"}]},"timestamp":"2025-10-21T15:36:32.166Z","tool":"chat","type":"evaluation"}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:81DC254371AE0875586F94B9CD7C3302","duration":2320,"evaluationId":"api-eval-1761061753677-7cmda78","name":"API Request","response":{"executionTime":2317,"metadata":{"evaluationId":"api-eval-1761061753677-7cmda78"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"How much is 5x7? Just respond with the number."},{"action":"final","answer":"35","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"35","timestamp":"2025-10-21T15:49:15.995Z"},"status":"success","toolCalls":[{"duration":2317,"status":"success","timestamp":"2025-10-21T15:49:15.995Z","tool":"chat"}]},"timestamp":"2025-10-21T15:49:15.997Z","tool":"chat","type":"evaluation"}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:8195D19F292418ABBD1AB3334B81DC05","duration":6238,"evaluationId":"api-eval-1761061969950-abrjv56","name":"API Request","response":{"executionTime":6233,"metadata":{"evaluationId":"api-eval-1761061969950-abrjv56"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"How much is 5x7? Just respond with the number."},{"action":"final","answer":"35","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"35","timestamp":"2025-10-21T15:52:56.187Z"},"status":"success","toolCalls":[{"duration":6233,"status":"success","timestamp":"2025-10-21T15:52:56.187Z","tool":"chat"}]},"timestamp":"2025-10-21T15:52:56.189Z","tool":"chat","type":"evaluation"}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:CDF4CD349852855B0A3EF25BC6B53D2B","duration":5874,"evaluationId":"api-eval-1761062047809-lsm91op","name":"API Request","response":{"executionTime":5870,"metadata":{"evaluationId":"api-eval-1761062047809-lsm91op"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"How much is 5x7? Just respond with the number."},{"action":"final","answer":"35","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"35","timestamp":"2025-10-21T15:54:13.680Z"},"status":"success","toolCalls":[{"duration":5870,"status":"success","timestamp":"2025-10-21T15:54:13.680Z","tool":"chat"}]},"timestamp":"2025-10-21T15:54:13.683Z","tool":"chat","type":"evaluation"}} +{"level":"info","message":{"clientId":"9907fd8d-92a8-4a6a-bce9-458ec8c57306:5739DB9B2476A699CB04C229E8BAB503","duration":2739,"evaluationId":"api-eval-1761062450183-yfkavrf","name":"API Request","response":{"executionTime":2733,"metadata":{"evaluationId":"api-eval-1761062450183-yfkavrf"},"output":{"evaluationMetadata":{"actualModelUsed":"gpt-5-mini","evaluationType":"chat"},"messages":[{"action":"final","answer":"Hello! I'm your AI assistant. How can I help you today?","entity":"model","isFinalAnswer":true},{"entity":"user","text":"How much is 5x7? Just respond with the number."},{"action":"final","answer":"35","entity":"model","isFinalAnswer":true,"reasoning":null}],"modelUsed":"gpt-5-mini","response":"35","timestamp":"2025-10-21T16:00:52.920Z"},"status":"success","toolCalls":[{"duration":2733,"status":"success","timestamp":"2025-10-21T16:00:52.920Z","tool":"chat"}]},"timestamp":"2025-10-21T16:00:52.924Z","tool":"chat","type":"evaluation"}} diff --git a/agent-server/nodejs/package-lock.json b/agent-server/nodejs/package-lock.json new file mode 100644 index 0000000000..b000a61ae2 --- /dev/null +++ b/agent-server/nodejs/package-lock.json @@ -0,0 +1,401 @@ +{ + "name": "bo-eval-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bo-eval-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "dotenv": "^16.3.1", + "js-yaml": "^4.1.0", + "uuid": "^9.0.1", + "winston": "^3.11.0", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/ws": "^8.5.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", + "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/agent-server/nodejs/package.json b/agent-server/nodejs/package.json new file mode 100644 index 0000000000..44d5270fdf --- /dev/null +++ b/agent-server/nodejs/package.json @@ -0,0 +1,37 @@ +{ + "name": "bo-eval-server", + "version": "1.0.0", + "description": "HTTP API wrapper for Browser Operator - WebSocket server with CDP integration", + "main": "src/lib/BrowserAgentServer.js", + "type": "module", + "exports": { + ".": "./src/lib/BrowserAgentServer.js", + "./BrowserAgentServer": "./src/lib/BrowserAgentServer.js", + "./HTTPWrapper": "./src/lib/HTTPWrapper.js" + }, + "scripts": { + "start": "node start.js" + }, + "keywords": [ + "websocket", + "browser-automation", + "cdp", + "http-api", + "rpc" + ], + "author": "", + "license": "MIT", + "dependencies": { + "dotenv": "^16.3.1", + "js-yaml": "^4.1.0", + "uuid": "^9.0.1", + "winston": "^3.11.0", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/ws": "^8.5.10" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/agent-server/nodejs/src/api-server.js b/agent-server/nodejs/src/api-server.js new file mode 100644 index 0000000000..bca27265b7 --- /dev/null +++ b/agent-server/nodejs/src/api-server.js @@ -0,0 +1,669 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import http from 'http'; +import url from 'url'; +import fs from 'fs'; +import path from 'path'; +import yaml from 'js-yaml'; +import { v4 as uuidv4 } from 'uuid'; + +import logger from './logger.js'; +// No need to import BrowserAgentServer - it's passed as constructor parameter + +class APIServer { + constructor(browserAgentServer, port = 8081) { + this.browserAgentServer = browserAgentServer; + this.port = port; + this.server = null; + this.configDefaults = null; + this.loadConfigDefaults(); + } + + /** + * Load default model configuration from config.yaml + */ + loadConfigDefaults() { + try { + const configPath = path.resolve('./evals/config.yaml'); + if (fs.existsSync(configPath)) { + const configContent = fs.readFileSync(configPath, 'utf8'); + this.configDefaults = yaml.load(configContent); + logger.info('Loaded config.yaml defaults:', this.configDefaults); + } else { + logger.warn('config.yaml not found, using hardcoded defaults'); + this.configDefaults = { + model: { + main_model: 'gpt-4.1', + mini_model: 'gpt-4.1-mini', + nano_model: 'gpt-4.1-nano', + provider: 'openai' + } + }; + } + } catch (error) { + logger.error('Failed to load config.yaml:', error); + this.configDefaults = { + model: { + main_model: 'gpt-4.1', + mini_model: 'gpt-4.1-mini', + nano_model: 'gpt-4.1-nano', + provider: 'openai' + } + }; + } + } + + start() { + this.server = http.createServer((req, res) => { + // Enable CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + this.handleRequest(req, res); + }); + + this.server.listen(this.port, () => { + logger.info(`API server started on http://localhost:${this.port}`); + }); + } + + async handleRequest(req, res) { + const parsedUrl = url.parse(req.url, true); + const pathname = parsedUrl.pathname; + const method = req.method; + + try { + // Get body for POST requests + let body = ''; + if (method === 'POST') { + for await (const chunk of req) { + body += chunk; + } + } + + let result; + + // Handle dynamic client tabs route + if (pathname.startsWith('/clients/') && pathname.endsWith('/tabs')) { + // Handle dynamic client tabs route + const clientId = pathname.split('/')[2]; + result = this.getClientTabsById(clientId); + } else { + switch (pathname) { + case '/status': + result = this.getStatus(); + break; + + case '/clients': + result = this.getClients(); + break; + + case '/tabs/open': + if (method !== 'POST') { + this.sendError(res, 405, 'Method not allowed'); + return; + } + result = await this.openTab(JSON.parse(body)); + break; + + case '/tabs/close': + if (method !== 'POST') { + this.sendError(res, 405, 'Method not allowed'); + return; + } + result = await this.closeTab(JSON.parse(body)); + break; + + case '/v1/responses': + if (method !== 'POST') { + this.sendError(res, 405, 'Method not allowed'); + return; + } + result = await this.handleResponsesRequest(JSON.parse(body)); + break; + + case '/page/content': + if (method !== 'POST') { + this.sendError(res, 405, 'Method not allowed'); + return; + } + result = await this.getPageContent(JSON.parse(body)); + break; + + case '/page/screenshot': + if (method !== 'POST') { + this.sendError(res, 405, 'Method not allowed'); + return; + } + result = await this.getScreenshot(JSON.parse(body)); + break; + + default: + this.sendError(res, 404, 'Not found'); + return; + } + } + + this.sendResponse(res, 200, result); + + } catch (error) { + logger.error('API error:', error); + this.sendError(res, 500, error.message); + } + } + + getStatus() { + const status = this.browserAgentServer.getStatus(); + const clients = this.browserAgentServer.getClientManager().getAllClients(); + + return { + server: status, + clients: clients.map(client => ({ + id: client.id, + name: client.name, + connected: this.browserAgentServer.connectedClients.has(client.id), + ready: this.browserAgentServer.connectedClients.get(client.id)?.ready || false + })) + }; + } + + getClients() { + const clients = this.browserAgentServer.getClientManager().getAllClients(); + const connectedClients = this.browserAgentServer.connectedClients; + + return clients.map(client => { + const tabs = this.browserAgentServer.getClientManager().getClientTabs(client.id); + + return { + id: client.id, + name: client.name, + description: client.description, + tabCount: tabs.length, + tabs: tabs.map(tab => ({ + tabId: tab.tabId, + compositeClientId: tab.compositeClientId, + connected: connectedClients.has(tab.compositeClientId), + ready: connectedClients.get(tab.compositeClientId)?.ready || false, + connectedAt: tab.connectedAt, + remoteAddress: tab.connection?.remoteAddress || 'unknown' + })) + }; + }); + } + + getClientTabsById(clientId) { + if (!clientId) { + throw new Error('Client ID is required'); + } + + const tabs = this.browserAgentServer.getClientManager().getClientTabs(clientId); + const connectedClients = this.browserAgentServer.connectedClients; + const client = this.browserAgentServer.getClientManager().getClient(clientId); + + if (!client) { + throw new Error(`Client '${clientId}' not found`); + } + + return { + baseClientId: clientId, + clientName: client.name, + tabCount: tabs.length, + tabs: tabs.map(tab => ({ + tabId: tab.tabId, + compositeClientId: tab.compositeClientId, + connected: connectedClients.has(tab.compositeClientId), + ready: connectedClients.get(tab.compositeClientId)?.ready || false, + connectedAt: tab.connectedAt, + remoteAddress: tab.connection?.remoteAddress || 'unknown' + })) + }; + } + + async openTab(payload) { + const { clientId, url = 'about:blank', background = false } = payload; + + if (!clientId) { + throw new Error('Client ID is required'); + } + + // Since we use direct CDP, we don't need the client to be connected + // Just extract the baseClientId (first part before colon if composite, or the whole ID) + const baseClientId = clientId.split(':')[0]; + + const result = await this.browserAgentServer.openTab(baseClientId, { url, background }); + + return { + clientId: baseClientId, + tabId: result.tabId, + compositeClientId: result.compositeClientId, + url: result.url || url, + status: 'opened' + }; + } + + async closeTab(payload) { + const { clientId, tabId } = payload; + + if (!clientId) { + throw new Error('Client ID is required'); + } + + if (!tabId) { + throw new Error('Tab ID is required'); + } + + // Since we use direct CDP, we don't need the client to be connected + // Just extract the baseClientId + const baseClientId = clientId.split(':')[0]; + + const result = await this.browserAgentServer.closeTab(baseClientId, { tabId }); + + return { + clientId: baseClientId, + tabId, + status: 'closed', + success: result.success !== false + }; + } + + async getPageContent(payload) { + const { clientId, tabId, format = 'html' } = payload; + + if (!clientId) { + throw new Error('Client ID is required'); + } + + if (!tabId) { + throw new Error('Tab ID is required'); + } + + if (!['html', 'text'].includes(format)) { + throw new Error('Format must be either "html" or "text"'); + } + + const baseClientId = clientId.split(':')[0]; + + logger.info('Getting page content', { baseClientId, tabId, format }); + + // Call appropriate method based on format + const result = format === 'html' + ? await this.browserAgentServer.getPageHTML(tabId) + : await this.browserAgentServer.getPageText(tabId); + + return { + clientId: baseClientId, + tabId: result.tabId, + content: result.content, + format: result.format, + length: result.length, + timestamp: Date.now() + }; + } + + async getScreenshot(payload) { + const { clientId, tabId, fullPage = false } = payload; + + if (!clientId) { + throw new Error('Client ID is required'); + } + + if (!tabId) { + throw new Error('Tab ID is required'); + } + + const baseClientId = clientId.split(':')[0]; + + logger.info('Capturing screenshot', { baseClientId, tabId, fullPage }); + + const result = await this.browserAgentServer.captureScreenshot(tabId, { fullPage }); + + return { + clientId: baseClientId, + tabId: result.tabId, + imageData: result.imageData, + format: result.format, + fullPage: result.fullPage, + timestamp: Date.now() + }; + } + + /** + * Handle OpenAI Responses API compatible requests with nested model format + */ + async handleResponsesRequest(requestBody) { + try { + // Validate required input field + if (!requestBody.input || typeof requestBody.input !== 'string') { + throw new Error('Missing or invalid "input" field. Expected a string.'); + } + + // Handle nested model configuration directly + const nestedModelConfig = this.processNestedModelConfig(requestBody); + + // Extract optional URL and wait timeout + const targetUrl = requestBody.url || 'about:blank'; + const waitTimeout = requestBody.wait_timeout || 5000; + + const redact = (mk) => ({ + ...mk, + api_key: mk?.api_key ? `${String(mk.api_key).slice(0, 4)}...` : undefined + }); + logger.info('Processing responses request:', { + input: requestBody.input, + url: targetUrl, + wait_timeout: targetUrl !== 'about:blank' ? waitTimeout : 0, + modelConfig: { + main_model: redact(nestedModelConfig.main_model), + mini_model: redact(nestedModelConfig.mini_model), + nano_model: redact(nestedModelConfig.nano_model), + } + }); + + // Find a client with existing tabs (not the dummy client) + const baseClientId = this.findClientWithTabs(); + + // Open a new tab for this request at the specified URL + logger.info('Opening new tab for responses request', { baseClientId, url: targetUrl }); + const tabResult = await this.browserAgentServer.openTab(baseClientId, { + url: targetUrl, + background: false + }); + + logger.info('Tab opened successfully', { + tabId: tabResult.tabId, + compositeClientId: tabResult.compositeClientId + }); + + // Wait for the new tab's DevTools to connect + const tabClient = await this.waitForClientConnection(tabResult.compositeClientId); + + // Wait for page to load if a custom URL was provided + if (targetUrl !== 'about:blank') { + logger.info('Waiting for page to load', { waitTimeout }); + await new Promise(resolve => setTimeout(resolve, waitTimeout)); + } + + // Create a dynamic request for this request + const request = this.createDynamicRequestNested(requestBody.input, nestedModelConfig); + + // Execute the request on the new tab's DevTools client + logger.info('Executing request on new tab', { + compositeClientId: tabResult.compositeClientId, + requestId: request.id + }); + + const result = await this.browserAgentServer.executeRequest(tabClient, request); + + // Debug: log the result structure + logger.debug('executeRequest result:', result); + + // Extract the response text from the result + const responseText = this.extractResponseText(result); + + // Format in OpenAI-compatible Responses API format with tab metadata + return this.formatResponse(responseText, tabResult.compositeClientId.split(':')[0], tabResult.tabId); + + } catch (error) { + logger.error('Error handling responses request:', error); + throw error; + } + } + + /** + * Process nested model configuration from request body + * @param {Object} requestBody - Request body containing optional model configuration + * @returns {import('./types/model-config').ModelConfig} Nested model configuration + */ + processNestedModelConfig(requestBody) { + const defaults = this.configDefaults?.model || {}; + + // If nested format is provided, use it directly with fallbacks + if (requestBody.model) { + return { + main_model: requestBody.model.main_model || this.createDefaultModelConfig('main', defaults), + mini_model: requestBody.model.mini_model || this.createDefaultModelConfig('mini', defaults), + nano_model: requestBody.model.nano_model || this.createDefaultModelConfig('nano', defaults) + }; + } + + // No model config provided, use defaults + return { + main_model: this.createDefaultModelConfig('main', defaults), + mini_model: this.createDefaultModelConfig('mini', defaults), + nano_model: this.createDefaultModelConfig('nano', defaults) + }; + } + + /** + * Create default model configuration for a tier + * @param {'main' | 'mini' | 'nano'} tier - Model tier + * @param {Object} defaults - Default configuration from config.yaml + * @returns {import('./types/model-config').ModelTierConfig} Model tier configuration + */ + createDefaultModelConfig(tier, defaults) { + const defaultModels = { + main: defaults.main_model || 'gpt-4', + mini: defaults.mini_model || 'gpt-4-mini', + nano: defaults.nano_model || 'gpt-3.5-turbo' + }; + + return { + provider: defaults.provider || 'openai', + model: defaultModels[tier], + api_key: process.env.OPENAI_API_KEY + }; + } + + + /** + * Find a connected and ready client + */ + findReadyClient() { + for (const [clientId, connection] of this.browserAgentServer.connectedClients) { + if (connection.ready) { + return connection; + } + } + return null; + } + + /** + * Find a client that has existing tabs (not the dummy client) + * @returns {string} Base client ID + */ + findClientWithTabs() { + const clients = this.browserAgentServer.getClientManager().getAllClients(); + + // First, try to find a client with existing tabs + for (const client of clients) { + const tabs = this.browserAgentServer.getClientManager().getClientTabs(client.id); + if (tabs.length > 0) { + logger.info('Found client with tabs', { clientId: client.id, tabCount: tabs.length }); + return client.id; + } + } + + // If no client with tabs, use the first available client (even with 0 tabs) + if (clients.length > 0) { + logger.info('No clients with tabs found, using first available client', { clientId: clients[0].id }); + return clients[0].id; + } + + throw new Error('No clients found. Please ensure at least one DevTools client is registered.'); + } + + /** + * Wait for a client connection to be established and ready + * @param {string} compositeClientId - Composite client ID (baseClientId:tabId) + * @param {number} maxWaitMs - Maximum time to wait in milliseconds + * @returns {Promise} Connection object + */ + async waitForClientConnection(compositeClientId, maxWaitMs = 10000) { + const startTime = Date.now(); + const pollInterval = 500; // Check every 500ms + + logger.info('Waiting for client connection', { compositeClientId, maxWaitMs }); + + while (Date.now() - startTime < maxWaitMs) { + const connection = this.browserAgentServer.connectedClients.get(compositeClientId); + + if (connection && connection.ready) { + logger.info('Client connection established and ready', { + compositeClientId, + waitedMs: Date.now() - startTime + }); + return connection; + } + + // Wait before next check + await new Promise(resolve => setTimeout(resolve, pollInterval)); + } + + throw new Error(`Timeout waiting for client connection: ${compositeClientId}. Tab may not have connected to eval-server.`); + } + + /** + * Create a dynamic evaluation object with nested model configuration + * @param {string} input - Input message for the evaluation + * @param {import('./types/model-config').ModelConfig} nestedModelConfig - Model configuration + * @returns {import('./types/model-config').EvaluationRequest} Evaluation request object + */ + createDynamicRequestNested(input, nestedModelConfig) { + const requestId = `api-req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + + return { + id: requestId, + name: 'API Request', + description: 'Dynamic request created from API request', + enabled: true, + tool: 'chat', + timeout: 7200000, // 2 hours (increased for slow custom API) + input: { + message: input + }, + model: nestedModelConfig, + validation: { + type: 'none' // No validation needed for API responses + }, + metadata: { + tags: ['api', 'dynamic'], + priority: 'high', + source: 'api' + } + }; + } + + + /** + * Extract response text from evaluation result + */ + extractResponseText(result) { + if (!result) { + return 'No response received from evaluation'; + } + + // Handle different result formats + if (typeof result === 'string') { + return result; + } + + // Check for nested evaluation result structure + if (result.output && result.output.response) { + return result.output.response; + } + + if (result.output && result.output.text) { + return result.output.text; + } + + if (result.output && result.output.answer) { + return result.output.answer; + } + + // Check top-level properties + if (result.response) { + return result.response; + } + + if (result.text) { + return result.text; + } + + if (result.answer) { + return result.answer; + } + + // If result is an object, try to extract meaningful content + if (typeof result === 'object') { + return JSON.stringify(result, null, 2); + } + + return 'Unable to extract response text from evaluation result'; + } + + /** + * Format response in OpenAI-compatible Responses API format + */ + formatResponse(responseText, clientId = null, tabId = null) { + const messageId = `msg_${uuidv4().replace(/-/g, '')}`; + + // Debug: log the parameters + logger.debug('formatResponse called with:', { clientId, tabId, hasClientId: !!clientId, hasTabId: !!tabId }); + + const response = [ + { + id: messageId, + type: 'message', + role: 'assistant', + content: [ + { + type: 'output_text', + text: responseText, + annotations: [] + } + ] + } + ]; + + // Add metadata if clientId and tabId are provided + if (clientId && tabId) { + response[0].metadata = { + clientId, + tabId + }; + logger.debug('Metadata added to response:', response[0].metadata); + } else { + logger.debug('Metadata NOT added - clientId or tabId missing'); + } + + return response; + } + + sendResponse(res, statusCode, data) { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data, null, 2)); + } + + sendError(res, statusCode, message) { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: message })); + } + + stop() { + if (this.server) { + this.server.close(); + logger.info('API server stopped'); + } + } +} + +export { APIServer }; diff --git a/eval-server/nodejs/src/client-manager.js b/agent-server/nodejs/src/client-manager.js similarity index 61% rename from eval-server/nodejs/src/client-manager.js rename to agent-server/nodejs/src/client-manager.js index d21b88dbd1..3c101c6b94 100644 --- a/eval-server/nodejs/src/client-manager.js +++ b/agent-server/nodejs/src/client-manager.js @@ -5,74 +5,17 @@ import { v4 as uuidv4 } from 'uuid'; import logger from './logger.js'; class ClientManager { - constructor(clientsDir = './clients', evalsDir = './evals') { + constructor(clientsDir = './clients') { this.clientsDir = path.resolve(clientsDir); - this.evalsDir = path.resolve(evalsDir); this.clients = new Map(); - this.evaluations = new Map(); // clientId -> evaluations array - this.configDefaults = null; // Config.yaml defaults for model precedence this.activeTabs = new Map(); // clientId -> Set of { tabId, connection, metadata } - + // Ensure directories exist if (!fs.existsSync(this.clientsDir)) { fs.mkdirSync(this.clientsDir, { recursive: true }); } - if (!fs.existsSync(this.evalsDir)) { - fs.mkdirSync(this.evalsDir, { recursive: true }); - } - - this.loadConfigDefaults(); - this.loadAllClients(); - this.loadAllEvaluations(); - } - - /** - * Load default model configuration from config.yaml - */ - loadConfigDefaults() { - try { - const configPath = path.resolve(this.evalsDir, 'config.yaml'); - if (fs.existsSync(configPath)) { - const configContent = fs.readFileSync(configPath, 'utf8'); - this.configDefaults = yaml.load(configContent); - logger.info('Loaded config.yaml defaults:', this.configDefaults); - } else { - // Don't warn about missing config.yaml - it's optional - this.configDefaults = null; - } - } catch (error) { - logger.error('Failed to load config.yaml:', error); - this.configDefaults = null; - } - } - - /** - * Apply model precedence: API calls OR test YAML models override config.yaml fallback - * Precedence logic: - * 1. API calls OR individual test YAML models (highest priority - either overrides everything) - * 2. config.yaml defaults (fallback only when neither API nor test YAML specify models) - * @param {Object} evaluation - Evaluation object with optional model configuration - * @param {import('../types/model-config').ModelConfig} apiModelOverride - Optional API model override - * @returns {import('../types/model-config').ModelConfig} Final model configuration - */ - applyModelPrecedence(evaluation, apiModelOverride = null) { - // Check if API override is provided - if (apiModelOverride) { - // API model override takes precedence over everything - // Ensure nested format is used - return apiModelOverride; - } - - // Check if evaluation has its own model config from YAML - const testModel = evaluation.model; - if (testModel && Object.keys(testModel).length > 0) { - // Test YAML model takes precedence - // Ensure nested format is returned - return testModel; - } - // Neither API nor test YAML specified models, use config.yaml defaults only - return this.configDefaults?.model || {}; + this.loadAllClients(); } /** @@ -130,80 +73,11 @@ class ClientManager { settings: config.settings || {}, yamlPath }); - - // Note: Evaluations are now loaded separately from the evals directory - // Initialize empty evaluations array for this client - if (!this.evaluations.has(clientId)) { - this.evaluations.set(clientId, []); - } - + logger.info(`Loaded client ${clientId}`); return config; } - /** - * Load all evaluations from the evals directory structure - */ - loadAllEvaluations() { - try { - // Clear existing evaluations to prevent duplicates on reload - this.evaluations.clear(); - - // Find all category directories - const categories = fs.readdirSync(this.evalsDir) - .filter(dir => fs.statSync(path.join(this.evalsDir, dir)).isDirectory()); - - let totalEvaluations = 0; - - for (const category of categories) { - const categoryDir = path.join(this.evalsDir, category); - const evalFiles = fs.readdirSync(categoryDir) - .filter(f => f.endsWith('.yaml') || f.endsWith('.yml')); - - for (const file of evalFiles) { - try { - const evalPath = path.join(categoryDir, file); - const yamlContent = fs.readFileSync(evalPath, 'utf8'); - const evaluation = yaml.load(yamlContent); - - if (evaluation.enabled !== false) { - // Apply model precedence: config.yaml overrides individual test models - const resolvedModel = this.applyModelPrecedence(evaluation); - - // Add evaluation to all clients for now - // In the future, you might want to have client-specific evaluation assignments - for (const [clientId] of this.clients) { - const clientEvals = this.evaluations.get(clientId) || []; - clientEvals.push({ - ...evaluation, - model: resolvedModel, // Use resolved model with precedence applied - clientId, - status: 'pending', - category, - filePath: evalPath - }); - this.evaluations.set(clientId, clientEvals); - } - totalEvaluations++; - } - } catch (error) { - logger.error(`Failed to load evaluation ${file}:`, error); - } - } - } - - // Update the client evaluation counts - for (const [clientId] of this.clients) { - const evalCount = this.evaluations.get(clientId)?.length || 0; - logger.info(`Loaded client ${clientId} with ${evalCount} evaluations`); - } - - logger.info(`Loaded ${totalEvaluations} evaluations from ${categories.length} categories`); - } catch (error) { - logger.error('Failed to load evaluations:', error); - } - } - /** * Register a new client with authentication */ @@ -225,8 +99,7 @@ class ClientManager { return { success: true, - clientName: client.name, - evaluationsCount: this.evaluations.get(clientId)?.length || 0 + clientName: client.name }; } @@ -237,38 +110,6 @@ class ClientManager { return this.clients.get(clientId); } - /** - * Get evaluations for a client - */ - getClientEvaluations(clientId) { - return this.evaluations.get(clientId) || []; - } - - /** - * Get next pending evaluation for a client - */ - getNextEvaluation(clientId) { - const evaluations = this.evaluations.get(clientId) || []; - return evaluations.find(e => e.status === 'pending'); - } - - /** - * Update evaluation status - */ - updateEvaluationStatus(clientId, evaluationId, status, result = null) { - const evaluations = this.evaluations.get(clientId); - if (!evaluations) return; - - const evaluation = evaluations.find(e => e.id === evaluationId); - if (evaluation) { - evaluation.status = status; - evaluation.lastRun = new Date().toISOString(); - if (result) { - evaluation.lastResult = result; - } - } - } - /** * Create a new client with default configuration */ @@ -283,35 +124,23 @@ class ClientManager { async createClientWithId(clientId, clientName, secretKey = null) { const yamlPath = path.join(this.clientsDir, `${clientId}.yaml`); - // Create simplified client configuration (evaluations come from evals directory) + // Create simplified client configuration const defaultConfig = { client: { id: clientId, name: clientName, secret_key: secretKey, - description: `Auto-generated DevTools evaluation client` - }, - settings: { - max_concurrent_evaluations: 3, - default_timeout: 45000, - retry_policy: { - max_retries: 2, - backoff_multiplier: 2, - initial_delay: 1000 - } + description: `Auto-generated DevTools client` } }; // Write YAML file const yamlContent = yaml.dump(defaultConfig, { indent: 2 }); fs.writeFileSync(yamlPath, yamlContent); - + // Load the new client this.loadClient(clientId); - - // Load evaluations for the new client - this.loadAllEvaluations(); - + logger.info(`Created new client: ${clientId}`); return { clientId, yamlPath }; } diff --git a/eval-server/nodejs/src/config.js b/agent-server/nodejs/src/config.js similarity index 85% rename from eval-server/nodejs/src/config.js rename to agent-server/nodejs/src/config.js index 4bde4e52d2..b273dcdbb3 100644 --- a/eval-server/nodejs/src/config.js +++ b/agent-server/nodejs/src/config.js @@ -45,8 +45,8 @@ export const CONFIG = { }, rpc: { - timeout: parseInt(process.env.RPC_TIMEOUT) || 1500000, // 25 minutes default - maxConcurrentEvaluations: parseInt(process.env.MAX_CONCURRENT_EVALUATIONS) || 10 + timeout: parseInt(process.env.RPC_TIMEOUT) || 7200000, // 2 hours default (increased for slow custom API) + maxConcurrentRequests: parseInt(process.env.MAX_CONCURRENT_REQUESTS) || 10 }, security: { @@ -59,6 +59,11 @@ export const CONFIG = { evals: { dir: process.env.EVALS_DIR || './evals' + }, + + cdp: { + host: process.env.CDP_HOST || 'localhost', + port: parseInt(process.env.CDP_PORT) || 9222 } }; diff --git a/eval-server/nodejs/src/lib/EvalServer.js b/agent-server/nodejs/src/lib/BrowserAgentServer.js similarity index 59% rename from eval-server/nodejs/src/lib/EvalServer.js rename to agent-server/nodejs/src/lib/BrowserAgentServer.js index d174c7a947..f4a43b2abb 100644 --- a/eval-server/nodejs/src/lib/EvalServer.js +++ b/agent-server/nodejs/src/lib/BrowserAgentServer.js @@ -8,40 +8,39 @@ import { WebSocketServer } from 'ws'; import { ClientManager } from '../client-manager.js'; import { CONFIG, validateConfig } from '../config.js'; -import logger, { logConnection, logEvaluation } from '../logger.js'; +import logger, { logConnection, logRequest } from '../logger.js'; import { RpcClient } from '../rpc-client.js'; -import { EvaluationLoader } from './EvaluationLoader.js'; /** - * EvalServer - A library for programmatically managing evaluation servers - * + * BrowserAgentServer - OpenAI-compatible HTTP API wrapper for Browser Operator + * * Example usage: * ```js - * const server = new EvalServer({ + * const server = new BrowserAgentServer({ * authKey: 'your-secret-key', * host: '127.0.0.1', * port: 8080 * }); - * + * * server.onConnect(client => { * console.log(`Client connected: ${client.id}`); - * - * client.evaluate({ - * id: "test_eval", - * name: "Bloomberg Eval", - * description: "Test Eval for Bloomberg website", + * + * client.execute({ + * id: "test_request", + * name: "Bloomberg Task", + * description: "Navigate to Bloomberg and summarize latest news", * input: { * objective: "Navigate to Bloomberg, summarize and return sentiment of the latest news." * } * }).then(response => { - * console.log('Evaluation response:', response); + * console.log('Request response:', response); * }); * }); - * + * * server.start(); * ``` */ -export class EvalServer extends EventEmitter { +export class BrowserAgentServer extends EventEmitter { constructor(options = {}) { super(); @@ -51,14 +50,12 @@ export class EvalServer extends EventEmitter { port: options.port || CONFIG.server.port, authKey: options.authKey || null, clientsDir: options.clientsDir || './clients', - evalsDir: options.evalsDir || './evals', ...options }; - + // Internal state this.connectedClients = new Map(); - this.clientManager = new ClientManager(this.config.clientsDir, this.config.evalsDir); - this.evaluationLoader = new EvaluationLoader(this.config.evalsDir); + this.clientManager = new ClientManager(this.config.clientsDir); this.judge = null; // Judge is optional - can be set later this.wss = null; this.isRunning = false; @@ -68,7 +65,7 @@ export class EvalServer extends EventEmitter { } /** - * Start the evaluation server + * Start the browser agent server */ async start() { if (this.isRunning) { @@ -94,14 +91,14 @@ export class EvalServer extends EventEmitter { }); this.isRunning = true; - logger.info(`Evaluation server started on ws://${this.config.host}:${this.config.port}`); + logger.info(`Browser agent server started on ws://${this.config.host}:${this.config.port}`); this.emit('started', { host: this.config.host, port: this.config.port }); return this; } /** - * Stop the evaluation server + * Stop the browser agent server */ async stop() { if (!this.isRunning) { @@ -123,7 +120,7 @@ export class EvalServer extends EventEmitter { this.connectedClients.clear(); this.isRunning = false; - logger.info('Evaluation server stopped'); + logger.info('Browser agent server stopped'); this.emit('stopped'); } @@ -146,8 +143,8 @@ export class EvalServer extends EventEmitter { } /** - * Set the judge for evaluations (optional) - * @param {Judge} judge - Judge instance for evaluation validation + * Set the judge for request validation (optional) + * @param {Judge} judge - Judge instance for request validation */ setJudge(judge) { // If server is already running, validate LLM config when setting judge @@ -157,7 +154,7 @@ export class EvalServer extends EventEmitter { throw new Error(`Cannot set judge: ${configErrors.join(', ')}`); } } - + this.judge = judge; return this; } @@ -182,20 +179,6 @@ export class EvalServer extends EventEmitter { }; } - /** - * Load evaluations from YAML files - */ - async loadEvaluations(evalsDir = './evals') { - return this.evaluationLoader.loadFromDirectory(evalsDir); - } - - /** - * Get all available evaluations - */ - getEvaluations() { - return this.evaluationLoader.getAllEvaluations(); - } - /** * Get the client manager instance */ @@ -297,7 +280,7 @@ export class EvalServer extends EventEmitter { return; } connection.ready = true; - logger.info('Client ready for evaluations', { + logger.info('Client ready for requests', { clientId: connection.clientId }); @@ -592,7 +575,7 @@ export class EvalServer extends EventEmitter { clientId, status: 'accepted', message: result.clientName ? `Welcome ${result.clientName}` : 'Client authenticated successfully', - evaluationsCount: result.evaluationsCount, + requestsCount: result.requestsCount, tabId: tabId, isComposite: isComposite }); @@ -621,21 +604,15 @@ export class EvalServer extends EventEmitter { handleStatusUpdate(connection, data) { if (!connection.registered) return; - const { evaluationId, status, progress, message } = data; + const { requestId, status, progress, message } = data; - logger.info('Evaluation status update', { + logger.info('Request status update', { clientId: connection.clientId, - evaluationId, + requestId, status, progress, message }); - - this.clientManager.updateEvaluationStatus( - connection.clientId, - evaluationId, - status - ); } /** @@ -696,28 +673,21 @@ export class EvalServer extends EventEmitter { } /** - * Execute evaluation on a specific client + * Execute request on a specific client */ - async executeEvaluation(connection, evaluation) { + async executeRequest(connection, request) { const startTime = Date.now(); const rpcId = `rpc-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; try { - logger.info('Starting evaluation', { + logger.info('Starting request', { clientId: connection.clientId, - evaluationId: evaluation.id, - tool: evaluation.tool + requestId: request.id, + tool: request.tool }); - // Update status to running - this.clientManager.updateEvaluationStatus( - connection.clientId, - evaluation.id, - 'running' - ); - - // Prepare model configuration - use client config if available, otherwise evaluation config, otherwise defaults - let modelConfig = evaluation.model || {}; + // Prepare model configuration - use client config if available, otherwise request config, otherwise defaults + let modelConfig = request.model || {}; if (connection.llmConfig) { // New nested format: separate config objects for each model tier @@ -740,7 +710,7 @@ export class EvalServer extends EventEmitter { api_key: connection.llmConfig.apiKey, endpoint: connection.llmConfig.endpoint }, - // Include any evaluation-specific overrides + // Include any request-specific overrides ...modelConfig }; } @@ -750,16 +720,16 @@ export class EvalServer extends EventEmitter { jsonrpc: '2.0', method: 'evaluate', params: { - evaluationId: evaluation.id, - name: evaluation.name, - url: evaluation.target?.url || evaluation.url, - tool: evaluation.tool, - input: evaluation.input, + requestId: request.id, + name: request.name, + url: request.target?.url || request.url, + tool: request.tool, + input: request.input, model: modelConfig, - timeout: evaluation.timeout || 30000, + timeout: request.timeout || 30000, metadata: { - tags: evaluation.metadata?.tags || [], - retries: evaluation.settings?.retry_policy?.max_retries || 0 + tags: request.metadata?.tags || [], + retries: request.settings?.retry_policy?.max_retries || 0 } }, id: rpcId @@ -770,35 +740,16 @@ export class EvalServer extends EventEmitter { connection.ws, 'evaluate', rpcRequest.params, - evaluation.timeout || 45000 + request.timeout || 45000 ); - // Validate response if needed and judge is available - let validationResult = null; - if (evaluation.validation && this.judge) { - validationResult = await this.validateResponse(response, evaluation); - } - - // Update evaluation status - this.clientManager.updateEvaluationStatus( - connection.clientId, - evaluation.id, - 'completed', - { - response, - validation: validationResult, - duration: Date.now() - startTime - } - ); - - // Log evaluation - logEvaluation({ - evaluationId: evaluation.id, + // Log request + logRequest({ + requestId: request.id, clientId: connection.clientId, - name: evaluation.name, - tool: evaluation.tool, + name: request.name, + tool: request.tool, response, - validation: validationResult, timestamp: new Date().toISOString(), duration: Date.now() - startTime }); @@ -806,64 +757,414 @@ export class EvalServer extends EventEmitter { return response; } catch (error) { - logger.error('Evaluation failed', { + logger.error('Request failed', { clientId: connection.clientId, - evaluationId: evaluation.id, + requestId: request.id, error: error.message }); - this.clientManager.updateEvaluationStatus( - connection.clientId, - evaluation.id, - 'failed', - { - error: error.message, - duration: Date.now() - startTime + throw error; + } + } + + /** + * Get the browser-level CDP WebSocket endpoint + * @returns {Promise} WebSocket URL + */ + async getCDPBrowserEndpoint() { + try { + const cdpUrl = `http://${CONFIG.cdp.host}:${CONFIG.cdp.port}/json/version`; + logger.info('Attempting to connect to CDP', { cdpUrl }); + const response = await fetch(cdpUrl); + const data = await response.json(); + return data.webSocketDebuggerUrl; + } catch (error) { + logger.error('Failed to get CDP browser endpoint', { error: error.message }); + throw new Error('Failed to connect to Chrome DevTools Protocol'); + } + } + + /** + * Send a CDP command via WebSocket + * @param {string} method - CDP method name + * @param {Object} params - CDP method parameters + * @returns {Promise} CDP response + */ + async sendCDPCommand(method, params = {}) { + return new Promise(async (resolve, reject) => { + try { + const { default: WebSocket } = await import('ws'); + const cdpEndpoint = await this.getCDPBrowserEndpoint(); + const ws = new WebSocket(cdpEndpoint); + // Use a simple counter for CDP message IDs (must be a reasonable integer) + const id = Math.floor(Math.random() * 1000000); + + const timeout = setTimeout(() => { + ws.close(); + reject(new Error(`CDP command timeout: ${method}`)); + }, 10000); + + ws.on('open', () => { + const message = JSON.stringify({ + id, + method, + params + }); + logger.info('CDP WebSocket opened, sending command', { method, params, cdpEndpoint }); + ws.send(message); + }); + + ws.on('message', (data) => { + try { + const response = JSON.parse(data.toString()); + logger.info('CDP WebSocket message received', { + method, + responseId: response.id, + expectedId: id, + hasResult: !!response.result, + hasError: !!response.error, + fullResponse: JSON.stringify(response) + }); + if (response.id === id) { + clearTimeout(timeout); + ws.close(); + + if (response.error) { + logger.error('CDP command error', { method, error: response.error }); + reject(new Error(`CDP error: ${response.error.message}`)); + } else { + logger.info('CDP command success', { method, result: response.result }); + resolve(response.result); + } + } else { + logger.warn('CDP message ID mismatch', { + method, + receivedId: response.id, + expectedId: id, + responseType: response.method ? 'event' : 'response' + }); + } + } catch (error) { + clearTimeout(timeout); + ws.close(); + logger.error('CDP message parse error', { error: error.message }); + reject(error); } - ); + }); + + ws.on('error', (error) => { + clearTimeout(timeout); + logger.error('CDP WebSocket error', { error: error.message }); + reject(error); + }); + } catch (error) { + reject(error); + } + }); + } + + /** + * Send a CDP command to a specific target (tab) + * This requires attaching to the target first, then detaching after + * @param {string} targetId - Target ID (tab ID) + * @param {string} method - CDP method name + * @param {Object} params - CDP method parameters + * @returns {Promise} CDP response + */ + async sendCDPCommandToTarget(targetId, method, params = {}) { + return new Promise(async (resolve, reject) => { + try { + const { default: WebSocket } = await import('ws'); + const cdpEndpoint = await this.getCDPBrowserEndpoint(); + const ws = new WebSocket(cdpEndpoint); + + let sessionId = null; + const attachId = Math.floor(Math.random() * 1000000); + const commandId = Math.floor(Math.random() * 1000000); + + const timeout = setTimeout(() => { + ws.close(); + reject(new Error(`CDP target command timeout: ${method} on ${targetId}`)); + }, 15000); + + ws.on('open', () => { + // First, attach to the target + const attachMessage = JSON.stringify({ + id: attachId, + method: 'Target.attachToTarget', + params: { + targetId, + flatten: true + } + }); + logger.info('CDP attaching to target', { targetId, method }); + ws.send(attachMessage); + }); + + ws.on('message', (data) => { + try { + const response = JSON.parse(data.toString()); + + // Handle attach response + if (response.id === attachId) { + if (response.error) { + clearTimeout(timeout); + ws.close(); + logger.error('CDP attach error', { targetId, error: response.error }); + reject(new Error(`CDP attach error: ${response.error.message}`)); + return; + } + + sessionId = response.result.sessionId; + logger.info('CDP attached to target, sending command', { sessionId, method }); + + // Now send the actual command with the session ID + const commandMessage = JSON.stringify({ + id: commandId, + method, + params, + sessionId + }); + ws.send(commandMessage); + } + + // Handle command response + else if (response.id === commandId) { + clearTimeout(timeout); + + if (response.error) { + logger.error('CDP target command error', { method, targetId, error: response.error }); + ws.close(); + reject(new Error(`CDP error: ${response.error.message}`)); + } else { + logger.info('CDP target command success', { method, targetId }); + ws.close(); + resolve(response.result); + } + } + // Ignore other messages (events, etc.) + } catch (error) { + clearTimeout(timeout); + ws.close(); + logger.error('CDP message parse error', { error: error.message }); + reject(error); + } + }); + + ws.on('error', (error) => { + clearTimeout(timeout); + logger.error('CDP WebSocket error', { error: error.message }); + reject(error); + }); + } catch (error) { + reject(error); + } + }); + } + + /** + * Open a new tab using CDP directly + * @param {string} baseClientId - Base client ID (or will be extracted from composite ID) + * @param {Object} options - Tab options + * @param {string} options.url - URL to open in the new tab (default: 'about:blank') + * @param {boolean} options.background - Whether to open in background (default: false) + * @returns {Promise} Result with tabId + */ + async openTab(baseClientId, options = {}) { + const { url = 'about:blank', background = false } = options; + // Extract base client ID if composite ID was passed + const cleanBaseClientId = baseClientId.split(':')[0]; + + try { + logger.info('Opening new tab via CDP', { url, background, baseClientId: cleanBaseClientId }); + + // Use CDP Target.createTarget + const result = await this.sendCDPCommand('Target.createTarget', { + url, + newWindow: false, + background + }); + + const tabId = result.targetId; + const compositeClientId = `${cleanBaseClientId}:${tabId}`; + logger.info('Tab opened successfully via CDP', { + tabId, + compositeClientId, + url + }); + + return { + tabId, + compositeClientId, + url + }; + } catch (error) { + logger.error('Failed to open tab via CDP', { + baseClientId, + url, + error: error.message + }); throw error; } } /** - * Validate response using configured judge + * Close a tab using CDP directly + * @param {string} baseClientId - Base client ID (currently not used, kept for API compatibility) + * @param {Object} options - Close options + * @param {string} options.tabId - Tab ID to close + * @returns {Promise} Result with success status */ - async validateResponse(response, evaluation) { - if (!this.judge) { - logger.warn('Validation requested but no judge configured'); + async closeTab(baseClientId, options = {}) { + const { tabId } = options; + + if (!tabId) { + throw new Error('tabId is required to close a tab'); + } + + try { + logger.info('Closing tab via CDP', { tabId, baseClientId }); + + // Use CDP Target.closeTarget + const result = await this.sendCDPCommand('Target.closeTarget', { + targetId: tabId + }); + + logger.info('Tab closed successfully via CDP', { + tabId, + success: result.success + }); + return { - type: 'no-judge', - result: { message: 'No judge configured for validation' }, - passed: true // Assume passed if no judge + success: result.success !== false, + tabId }; + } catch (error) { + logger.error('Failed to close tab via CDP', { + tabId, + baseClientId, + error: error.message + }); + throw error; } + } - const validation = evaluation.validation; + /** + * Get page HTML content using CDP + * @param {string} tabId - Tab ID (target ID) + * @returns {Promise} Result with HTML content + */ + async getPageHTML(tabId) { + try { + logger.info('Getting page HTML via CDP', { tabId }); - if (validation.type === 'llm-judge' || validation.type === 'hybrid') { - const llmConfig = validation.llm_judge || validation.llmJudge; - const criteria = llmConfig?.criteria || []; - const task = `${evaluation.name} - ${evaluation.description || ''}`; + // Use Runtime.evaluate to get document.documentElement.outerHTML + const result = await this.sendCDPCommandToTarget(tabId, 'Runtime.evaluate', { + expression: 'document.documentElement.outerHTML', + returnByValue: true + }); - const judgeResult = await this.judge.evaluate( - task, - JSON.stringify(response.output || response), - { - criteria, - model: llmConfig?.model - } - ); + const html = result.result.value; + + logger.info('Page HTML retrieved successfully', { + tabId, + length: html.length + }); + + return { + tabId, + content: html, + format: 'html', + length: html.length + }; + } catch (error) { + logger.error('Failed to get page HTML via CDP', { + tabId, + error: error.message + }); + throw error; + } + } + + /** + * Get page text content using CDP + * @param {string} tabId - Tab ID (target ID) + * @returns {Promise} Result with text content + */ + async getPageText(tabId) { + try { + logger.info('Getting page text via CDP', { tabId }); + + // Use Runtime.evaluate to get document.body.innerText + const result = await this.sendCDPCommandToTarget(tabId, 'Runtime.evaluate', { + expression: 'document.body.innerText', + returnByValue: true + }); + + const text = result.result.value; + + logger.info('Page text retrieved successfully', { + tabId, + length: text.length + }); return { - type: 'llm-judge', - result: judgeResult, - passed: judgeResult.score >= 0.7 + tabId, + content: text, + format: 'text', + length: text.length }; + } catch (error) { + logger.error('Failed to get page text via CDP', { + tabId, + error: error.message + }); + throw error; } + } + + /** + * Capture page screenshot using CDP + * @param {string} tabId - Tab ID (target ID) + * @param {Object} options - Screenshot options + * @param {boolean} options.fullPage - Whether to capture full page (default: false) + * @returns {Promise} Result with screenshot data + */ + async captureScreenshot(tabId, options = {}) { + const { fullPage = false } = options; - return null; + try { + logger.info('Capturing screenshot via CDP', { tabId, fullPage }); + + // Use Page.captureScreenshot + const result = await this.sendCDPCommandToTarget(tabId, 'Page.captureScreenshot', { + format: 'png', + captureBeyondViewport: fullPage + }); + + const imageData = `data:image/png;base64,${result.data}`; + + logger.info('Screenshot captured successfully', { + tabId, + dataLength: result.data.length + }); + + return { + tabId, + imageData, + format: 'png', + fullPage + }; + } catch (error) { + logger.error('Failed to capture screenshot via CDP', { + tabId, + error: error.message + }); + throw error; + } } + } /** @@ -879,25 +1180,33 @@ class ClientProxy { } /** - * Execute an evaluation on this client + * Execute a request on this client */ - async evaluate(evaluation) { - // Ensure evaluation has required fields - const fullEvaluation = { - id: evaluation.id || `eval-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, - name: evaluation.name || 'Dynamic Evaluation', - description: evaluation.description || 'Programmatically created evaluation', + async execute(request) { + // Ensure request has required fields + const fullRequest = { + id: request.id || `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + name: request.name || 'Dynamic Request', + description: request.description || 'Programmatically created request', enabled: true, - tool: evaluation.tool || 'chat', - timeout: evaluation.timeout || 45000, - input: evaluation.input || {}, - model: evaluation.model || {}, - validation: evaluation.validation || { type: 'none' }, - metadata: evaluation.metadata || { tags: ['api', 'dynamic'] }, - ...evaluation + tool: request.tool || 'chat', + timeout: request.timeout || 45000, + input: request.input || {}, + model: request.model || {}, + validation: request.validation || { type: 'none' }, + metadata: request.metadata || { tags: ['api', 'dynamic'] }, + ...request }; - return this.server.executeEvaluation(this.connection, fullEvaluation); + return this.server.executeRequest(this.connection, fullRequest); + } + + /** + * Alias for backward compatibility + * @deprecated Use execute() instead + */ + async evaluate(request) { + return this.execute(request); } /** diff --git a/eval-server/nodejs/src/lib/HTTPWrapper.js b/agent-server/nodejs/src/lib/HTTPWrapper.js similarity index 66% rename from eval-server/nodejs/src/lib/HTTPWrapper.js rename to agent-server/nodejs/src/lib/HTTPWrapper.js index f377690b6f..c3653c5c1e 100644 --- a/eval-server/nodejs/src/lib/HTTPWrapper.js +++ b/agent-server/nodejs/src/lib/HTTPWrapper.js @@ -5,33 +5,33 @@ import { APIServer } from '../api-server.js'; /** - * HTTPWrapper - Optional HTTP API wrapper for EvalServer - * - * This provides an HTTP REST API on top of the core EvalServer, + * HTTPWrapper - Optional HTTP API wrapper for BrowserAgentServer + * + * This provides an HTTP REST API on top of the core BrowserAgentServer, * following the same pattern as the CLI wrapper. - * + * * Example usage: * ```js - * import { EvalServer } from './EvalServer.js'; + * import { BrowserAgentServer } from './BrowserAgentServer.js'; * import { HTTPWrapper } from './HTTPWrapper.js'; - * - * const evalServer = new EvalServer({ port: 8080 }); - * const httpWrapper = new HTTPWrapper(evalServer, { port: 8081 }); - * - * await evalServer.start(); + * + * const browserAgentServer = new BrowserAgentServer({ port: 8080 }); + * const httpWrapper = new HTTPWrapper(browserAgentServer, { port: 8081 }); + * + * await browserAgentServer.start(); * await httpWrapper.start(); * ``` */ export class HTTPWrapper { - constructor(evalServer, options = {}) { - this.evalServer = evalServer; + constructor(browserAgentServer, options = {}) { + this.browserAgentServer = browserAgentServer; this.config = { port: options.port || 8081, host: options.host || 'localhost', ...options }; - - this.apiServer = new APIServer(evalServer, this.config.port); + + this.apiServer = new APIServer(browserAgentServer, this.config.port); this.isRunning = false; } @@ -43,8 +43,8 @@ export class HTTPWrapper { throw new Error('HTTP wrapper is already running'); } - if (!this.evalServer.isRunning) { - throw new Error('EvalServer must be started before starting HTTP wrapper'); + if (!this.browserAgentServer.isRunning) { + throw new Error('BrowserAgentServer must be started before starting HTTP wrapper'); } this.apiServer.start(); diff --git a/agent-server/nodejs/src/lib/RequestStack.js b/agent-server/nodejs/src/lib/RequestStack.js new file mode 100644 index 0000000000..9eb363b42a --- /dev/null +++ b/agent-server/nodejs/src/lib/RequestStack.js @@ -0,0 +1,85 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * RequestStack - A simple stack-like structure for managing requests + * + * Provides LIFO (Last In, First Out) access to request objects. + * Useful for distributing different requests across multiple client connections. + */ +export class RequestStack { + constructor() { + this.requests = []; + } + + /** + * Add a request to the top of the stack + * @param {Object} request - The request object to add + */ + push(request) { + if (!request || typeof request !== 'object') { + throw new Error('Request must be a valid object'); + } + + // Validate required fields + const requiredFields = ['id', 'name', 'tool', 'input']; + for (const field of requiredFields) { + if (!request[field]) { + throw new Error(`Request missing required field: ${field}`); + } + } + + this.requests.push(request); + } + + /** + * Remove and return the request from the top of the stack + * @returns {Object|null} The request object, or null if stack is empty + */ + pop() { + return this.requests.pop() || null; + } + + /** + * Check if the stack is empty + * @returns {boolean} True if stack has no requests + */ + isEmpty() { + return this.requests.length === 0; + } + + /** + * Get the number of requests in the stack + * @returns {number} The stack size + */ + size() { + return this.requests.length; + } + + /** + * Peek at the top request without removing it + * @returns {Object|null} The top request object, or null if stack is empty + */ + peek() { + if (this.isEmpty()) { + return null; + } + return this.requests[this.requests.length - 1]; + } + + /** + * Clear all requests from the stack + */ + clear() { + this.requests = []; + } + + /** + * Get a copy of all requests in the stack (top to bottom) + * @returns {Array} Array of request objects + */ + toArray() { + return [...this.requests].reverse(); + } +} \ No newline at end of file diff --git a/eval-server/nodejs/src/lib/judges/Judge.js b/agent-server/nodejs/src/lib/judges/Judge.js similarity index 100% rename from eval-server/nodejs/src/lib/judges/Judge.js rename to agent-server/nodejs/src/lib/judges/Judge.js diff --git a/eval-server/nodejs/src/lib/judges/LLMJudge.js b/agent-server/nodejs/src/lib/judges/LLMJudge.js similarity index 100% rename from eval-server/nodejs/src/lib/judges/LLMJudge.js rename to agent-server/nodejs/src/lib/judges/LLMJudge.js diff --git a/eval-server/nodejs/src/logger.js b/agent-server/nodejs/src/logger.js similarity index 59% rename from eval-server/nodejs/src/logger.js rename to agent-server/nodejs/src/logger.js index c935eb9ff7..dba2fe1616 100644 --- a/eval-server/nodejs/src/logger.js +++ b/agent-server/nodejs/src/logger.js @@ -32,40 +32,40 @@ const logger = winston.createLogger({ ] }); -// Create dedicated evaluation logger once to avoid recreating on each call -const evaluationLogger = winston.createLogger({ +// Create dedicated request logger once to avoid recreating on each call +const requestLogger = winston.createLogger({ format: winston.format.json(), transports: [ new winston.transports.File({ - filename: `${CONFIG.logging.dir}/evaluations.jsonl` + filename: `${CONFIG.logging.dir}/requests.jsonl` }) ] }); -export function logEvaluation(evaluationData) { +export function logRequest(requestData) { const logEntry = { - type: 'evaluation', + type: 'request', timestamp: new Date().toISOString(), - ...evaluationData + ...requestData }; - - // Pretty print evaluation summary to console + + // Pretty print request summary to console console.log('\n' + '='.repeat(80)); - console.log(`📊 EVALUATION COMPLETED: ${evaluationData.name}`); + console.log(`📊 REQUEST COMPLETED: ${requestData.name}`); console.log('='.repeat(80)); - console.log(`🆔 ID: ${evaluationData.evaluationId}`); - console.log(`🔧 Tool: ${evaluationData.tool}`); - console.log(`âąī¸ Duration: ${evaluationData.duration}ms`); - console.log(`👤 Client: ${evaluationData.clientId}`); - - if (evaluationData.response?.output?.output) { - console.log(`\n📝 Output:\n${evaluationData.response.output.output}`); + console.log(`🆔 ID: ${requestData.requestId}`); + console.log(`🔧 Tool: ${requestData.tool}`); + console.log(`âąī¸ Duration: ${requestData.duration}ms`); + console.log(`👤 Client: ${requestData.clientId}`); + + if (requestData.response?.output?.output) { + console.log(`\n📝 Output:\n${requestData.response.output.output}`); } - - if (evaluationData.validation?.result) { - const val = evaluationData.validation.result; + + if (requestData.validation?.result) { + const val = requestData.validation.result; console.log(`\n📋 Validation:`); - console.log(` ✅ Passed: ${evaluationData.validation.passed ? 'YES' : 'NO'}`); + console.log(` ✅ Passed: ${requestData.validation.passed ? 'YES' : 'NO'}`); console.log(` 📊 Overall Score: ${val.overall_score}/10`); if (val.strengths?.length > 0) { console.log(` đŸ’Ē Strengths: ${val.strengths.join(', ')}`); @@ -74,14 +74,25 @@ export function logEvaluation(evaluationData) { console.log(` âš ī¸ Weaknesses: ${val.weaknesses.join(', ')}`); } } - + console.log('='.repeat(80) + '\n'); - + // Also log structured data for file logs - logger.info('Evaluation completed', logEntry); - - // Also save to dedicated evaluation log - evaluationLogger.info(logEntry); + logger.info('Request completed', logEntry); + + // Also save to dedicated request log + requestLogger.info(logEntry); +} + +// Backward compatibility alias +export function logEvaluation(evaluationData) { + // Map evaluationId to requestId if present + const requestData = { ...evaluationData }; + if (evaluationData.evaluationId && !evaluationData.requestId) { + requestData.requestId = evaluationData.evaluationId; + delete requestData.evaluationId; + } + return logRequest(requestData); } export function logRpcCall(callData) { diff --git a/eval-server/nodejs/src/rpc-client.js b/agent-server/nodejs/src/rpc-client.js similarity index 100% rename from eval-server/nodejs/src/rpc-client.js rename to agent-server/nodejs/src/rpc-client.js diff --git a/agent-server/nodejs/start.js b/agent-server/nodejs/start.js new file mode 100644 index 0000000000..9a0bc5fff4 --- /dev/null +++ b/agent-server/nodejs/start.js @@ -0,0 +1,39 @@ +import { BrowserAgentServer } from "./src/lib/BrowserAgentServer.js"; +import { HTTPWrapper } from "./src/lib/HTTPWrapper.js"; + +const WS_PORT = parseInt(process.env.PORT || "8082"); +const HTTP_PORT = parseInt(process.env.API_PORT || "8081"); +const HOST = process.env.HOST || "0.0.0.0"; + +console.log("🔧 Creating BrowserAgentServer..."); +const browserAgentServer = new BrowserAgentServer({ + host: HOST, + port: WS_PORT +}); + +console.log("🔧 Creating HTTP wrapper..."); +const httpWrapper = new HTTPWrapper(browserAgentServer, { + port: HTTP_PORT, + host: HOST +}); + +console.log("🔧 Starting BrowserAgentServer..."); +await browserAgentServer.start(); +console.log(`✅ BrowserAgentServer started on ws://${HOST}:${WS_PORT}`); + +console.log("🔧 Starting HTTP wrapper..."); +await httpWrapper.start(); +console.log(`✅ HTTP API started on http://${HOST}:${HTTP_PORT}`); + +console.log("âŗ Server ready for connections..."); + +// Keep process alive +process.on('SIGTERM', () => { + console.log('Received SIGTERM, shutting down gracefully...'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('Received SIGINT, shutting down gracefully...'); + process.exit(0); +}); diff --git a/eval-server/nodejs/examples/with-http-wrapper.js b/agent-server/start.js similarity index 55% rename from eval-server/nodejs/examples/with-http-wrapper.js rename to agent-server/start.js index 2ec9d0f16c..f678a930f1 100644 --- a/eval-server/nodejs/examples/with-http-wrapper.js +++ b/agent-server/start.js @@ -1,39 +1,39 @@ #!/usr/bin/env node -// Copyright 2025 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +// Custom browser-agent-server startup script for Cloud Run +// Uses environment variables for port configuration -// Example demonstrating how to use EvalServer with optional HTTP API wrapper +import { EvalServer } from './src/lib/EvalServer.js'; +import { HTTPWrapper } from './src/lib/HTTPWrapper.js'; -import { EvalServer } from '../src/lib/EvalServer.js'; -import { HTTPWrapper } from '../src/lib/HTTPWrapper.js'; +const WS_PORT = parseInt(process.env.EVAL_SERVER_WS_PORT || '8082'); +const HTTP_PORT = parseInt(process.env.EVAL_SERVER_HTTP_PORT || '8083'); +const HOST = process.env.EVAL_SERVER_HOST || '127.0.0.1'; console.log('🔧 Creating EvalServer...'); const evalServer = new EvalServer({ // No authKey - authentication disabled for automated mode - host: '127.0.0.1', - port: 8082 + host: HOST, + port: WS_PORT }); console.log('🔧 Creating HTTP wrapper...'); const httpWrapper = new HTTPWrapper(evalServer, { - port: 8080, - host: '127.0.0.1' + port: HTTP_PORT, + host: HOST }); - console.log('🔧 Starting EvalServer...'); await evalServer.start(); -console.log('✅ EvalServer started on ws://127.0.0.1:8082'); +console.log(`✅ EvalServer started on ws://${HOST}:${WS_PORT}`); console.log('🔧 Starting HTTP wrapper...'); await httpWrapper.start(); -console.log('✅ HTTP API started on http://127.0.0.1:8080'); +console.log(`✅ HTTP API started on http://${HOST}:${HTTP_PORT}`); console.log('âŗ Waiting for DevTools client to connect...'); -console.log(' WebSocket URL: ws://127.0.0.1:8082'); -console.log(' HTTP API URL: http://127.0.0.1:8080'); +console.log(` WebSocket URL: ws://${HOST}:${WS_PORT}`); +console.log(` HTTP API URL: http://${HOST}:${HTTP_PORT}`); console.log(' Auth: Disabled (automated mode)'); // Add periodic status check @@ -42,4 +42,4 @@ setInterval(() => { const httpWrapperStatus = httpWrapper.getStatus(); console.log(`📊 EvalServer: ${evalServerStatus.connectedClients} clients, ${evalServerStatus.readyClients} ready`); console.log(`📊 HTTP API: ${httpWrapperStatus.isRunning ? 'running' : 'stopped'} on ${httpWrapperStatus.url}`); -}, 15000); \ No newline at end of file +}, 30000); diff --git a/eval-server/.gitignore b/eval-server/.gitignore deleted file mode 100644 index 78e7d645ff..0000000000 --- a/eval-server/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.env -node_modules -*.log diff --git a/eval-server/README.md b/eval-server/README.md deleted file mode 100644 index 88c852f46d..0000000000 --- a/eval-server/README.md +++ /dev/null @@ -1,239 +0,0 @@ -# Eval-Server - -A WebSocket-based evaluation server for LLM agents with multiple language implementations. - -## Overview - -This directory contains two functionally equivalent implementations of the bo-eval-server: - -- **NodeJS** (`nodejs/`) - Full-featured implementation with YAML evaluations, HTTP API, CLI, and judge system -- **Python** (`python/`) - Minimal library focused on core WebSocket functionality and programmatic evaluation creation - -Both implementations provide: -- 🔌 **WebSocket Server** - Real-time agent connections -- 🤖 **Bidirectional RPC** - JSON-RPC 2.0 for calling agent methods -- 📚 **Programmatic API** - Create and manage evaluations in code -- ⚡ **Concurrent Support** - Handle multiple agents simultaneously -- 📊 **Structured Logging** - Comprehensive evaluation tracking - -## Quick Start - -### NodeJS (Full Featured) - -The NodeJS implementation includes YAML evaluation loading, HTTP API wrapper, CLI tools, and LLM-as-a-judge functionality. - -```bash -cd nodejs/ -npm install -npm start -``` - -**Key Features:** -- YAML evaluation file loading -- HTTP API wrapper for REST integration -- Interactive CLI for management -- LLM judge system for response evaluation -- Comprehensive documentation and examples - -See [`nodejs/README.md`](nodejs/README.md) for detailed usage. - -### Python (Lightweight Library) - -The Python implementation focuses on core WebSocket functionality with programmatic evaluation creation. - -```bash -cd python/ -pip install -e . -python examples/basic_server.py -``` - -**Key Features:** -- Minimal dependencies (websockets, loguru) -- Full async/await support -- Evaluation stack for LIFO queuing -- Type hints throughout -- Clean Pythonic API - -See [`python/README.md`](python/README.md) for detailed usage. - -## Architecture Comparison - -| Feature | NodeJS | Python | -|---------|--------|--------| -| **Core WebSocket Server** | ✅ | ✅ | -| **JSON-RPC 2.0** | ✅ | ✅ | -| **Client Management** | ✅ | ✅ | -| **Programmatic Evaluations** | ✅ | ✅ | -| **Evaluation Stack** | ✅ | ✅ | -| **Structured Logging** | ✅ (Winston) | ✅ (Loguru) | -| **YAML Evaluations** | ✅ | ❌ | -| **HTTP API Wrapper** | ✅ | ❌ | -| **CLI Interface** | ✅ | ❌ | -| **LLM Judge System** | ✅ | ❌ | -| **Type System** | TypeScript | Type Hints | - -## Choosing an Implementation - -**Choose NodeJS if you need:** -- YAML-based evaluation definitions -- HTTP REST API endpoints -- Interactive CLI for management -- LLM-as-a-judge evaluation -- Comprehensive feature set - -**Choose Python if you need:** -- Minimal dependencies -- Pure programmatic approach -- Integration with Python ML pipelines -- Modern async/await patterns -- Lightweight deployment - -## Agent Protocol - -Both implementations use the same WebSocket protocol: - -### 1. Connect to WebSocket -```javascript -// NodeJS -const ws = new WebSocket('ws://localhost:8080'); - -// Python -import websockets -ws = await websockets.connect('ws://localhost:8080') -``` - -### 2. Send Registration -```json -{ - "type": "register", - "clientId": "your-client-id", - "secretKey": "your-secret-key", - "capabilities": ["chat", "action"] -} -``` - -### 3. Send Ready Signal -```json -{ - "type": "ready" -} -``` - -### 4. Handle RPC Calls -Both implementations send JSON-RPC 2.0 requests with the `evaluate` method: - -```json -{ - "jsonrpc": "2.0", - "method": "evaluate", - "params": { - "id": "eval_001", - "name": "Test Evaluation", - "tool": "chat", - "input": {"message": "Hello world"} - }, - "id": "unique-call-id" -} -``` - -Agents should respond with: -```json -{ - "jsonrpc": "2.0", - "id": "unique-call-id", - "result": { - "status": "completed", - "output": {"response": "Hello! How can I help you?"} - } -} -``` - -## Examples - -### NodeJS Example -```javascript -import { EvalServer } from 'bo-eval-server'; - -const server = new EvalServer({ - authKey: 'secret', - port: 8080 -}); - -server.onConnect(async client => { - const result = await client.evaluate({ - id: "test", - name: "Hello World", - tool: "chat", - input: {message: "Hi there!"} - }); - console.log(result); -}); - -await server.start(); -``` - -### Python Example -```python -import asyncio -from bo_eval_server import EvalServer - -async def main(): - server = EvalServer( - auth_key='secret', - port=8080 - ) - - @server.on_connect - async def handle_client(client): - result = await client.evaluate({ - "id": "test", - "name": "Hello World", - "tool": "chat", - "input": {"message": "Hi there!"} - }) - print(result) - - await server.start() - await server.wait_closed() - -asyncio.run(main()) -``` - -## Development - -Each implementation has its own development setup: - -**NodeJS:** -```bash -cd nodejs/ -npm install -npm run dev # Watch mode -npm test # Run tests -npm run cli # Interactive CLI -``` - -**Python:** -```bash -cd python/ -pip install -e ".[dev]" -pytest # Run tests -black . # Format code -mypy src/ # Type checking -``` - -## Contributing - -When contributing to either implementation: - -1. Maintain API compatibility between versions where possible -2. Update documentation for both implementations when adding shared features -3. Follow the existing code style and patterns -4. Add appropriate tests and examples - -## License - -MIT License - see individual implementation directories for details. - ---- - -Both implementations provide robust, production-ready evaluation servers for LLM agents with different feature sets optimized for different use cases. \ No newline at end of file diff --git a/eval-server/nodejs/CLAUDE.md b/eval-server/nodejs/CLAUDE.md deleted file mode 100644 index ba84f31b0b..0000000000 --- a/eval-server/nodejs/CLAUDE.md +++ /dev/null @@ -1,190 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -bo-eval-server is a WebSocket-based evaluation server for LLM agents that implements an LLM-as-a-judge evaluation system. The server accepts connections from AI agents, sends them evaluation tasks via RPC calls, collects their responses, and uses an LLM to judge the quality of responses. - -## Commands - -### Development -- `npm start` - Start the WebSocket server -- `npm run dev` - Start server with file watching for development -- `npm run cli` - Start interactive CLI for server management and testing -- `npm test` - Run example agent client for testing - -### Installation -- `npm install` - Install dependencies -- Copy `.env.example` to `.env` and configure environment variables - -### Required Environment Variables -- `OPENAI_API_KEY` - OpenAI API key for LLM judge functionality -- `PORT` - WebSocket server port (default: 8080) - -### LLM Provider Configuration (Optional) -- `GROQ_API_KEY` - Groq API key for Groq provider support -- `OPENROUTER_API_KEY` - OpenRouter API key for OpenRouter provider support -- `LITELLM_ENDPOINT` - LiteLLM server endpoint URL -- `LITELLM_API_KEY` - LiteLLM API key for LiteLLM provider support -- `DEFAULT_PROVIDER` - Default LLM provider (openai, groq, openrouter, litellm) -- `DEFAULT_MAIN_MODEL` - Default main model name -- `DEFAULT_MINI_MODEL` - Default mini model name -- `DEFAULT_NANO_MODEL` - Default nano model name - -## Architecture - -### Core Components - -**WebSocket Server** (`src/server.js`) -- Accepts connections from LLM agents -- Manages agent lifecycle (connect, ready, disconnect) -- Orchestrates evaluation sessions -- Handles bidirectional RPC communication - -**RPC Client** (`src/rpc-client.js`) -- Implements JSON-RPC 2.0 protocol for bidirectional communication -- Manages request/response correlation with unique IDs -- Handles timeouts and error conditions -- Calls `Evaluate(request: String) -> String` method on connected agents -- Supports `configure_llm` method for dynamic LLM provider configuration - -**LLM Evaluator** (`src/evaluator.js`) -- Integrates with OpenAI API for LLM-as-a-judge functionality -- Evaluates agent responses on multiple criteria (correctness, completeness, clarity, relevance, helpfulness) -- Returns structured JSON evaluation with scores and reasoning - -**Logger** (`src/logger.js`) -- Structured logging using Winston -- Separate log files for different event types -- JSON format for easy parsing and analysis -- Logs all RPC calls, evaluations, and connection events - -### Evaluation Flow - -1. Agent connects to WebSocket server -2. Agent sends "ready" signal -3. Server calls agent's `Evaluate` method with a task -4. Agent processes task and returns response -5. Server sends response to LLM judge for evaluation -6. Results are logged as JSON with scores and detailed feedback - -### Project Structure - -``` -src/ -├── server.js # Main WebSocket server and evaluation orchestration -├── rpc-client.js # JSON-RPC client for calling agent methods -├── evaluator.js # LLM judge integration (OpenAI) -├── logger.js # Structured logging and result storage -├── config.js # Configuration management -└── cli.js # Interactive CLI for testing and management - -logs/ # Log files (created automatically) -├── combined.log # All log events -├── error.log # Error events only -└── evaluations.jsonl # Evaluation results in JSON Lines format -``` - -### Key Features - -- **Bidirectional RPC**: Server can call methods on connected clients -- **Multi-Provider LLM Support**: Support for OpenAI, Groq, OpenRouter, and LiteLLM providers -- **Dynamic LLM Configuration**: Runtime configuration via `configure_llm` JSON-RPC method -- **Per-Client Configuration**: Each connected client can have different LLM settings -- **LLM-as-a-Judge**: Automated evaluation of agent responses using configurable LLM providers -- **Concurrent Evaluations**: Support for multiple agents and parallel evaluations -- **Structured Logging**: All interactions logged as JSON for analysis -- **Interactive CLI**: Built-in CLI for testing and server management -- **Connection Management**: Robust handling of agent connections and disconnections -- **Timeout Handling**: Configurable timeouts for RPC calls and evaluations - -### Agent Protocol - -Agents must implement: -- WebSocket connection to server -- JSON-RPC 2.0 protocol support -- `Evaluate(task: string) -> string` method -- "ready" message to signal availability for evaluations - -### Model Configuration Schema - -The server uses a canonical nested model configuration format that allows per-tier provider and API key settings: - -#### Model Configuration Structure - -```typescript -interface ModelTierConfig { - provider: string; // "openai" | "groq" | "openrouter" | "litellm" - model: string; // Model name (e.g., "gpt-4", "llama-3.1-8b-instant") - api_key: string; // API key for this tier -} - -interface ModelConfig { - main_model: ModelTierConfig; // Primary model for complex tasks - mini_model: ModelTierConfig; // Secondary model for simpler tasks - nano_model: ModelTierConfig; // Tertiary model for basic tasks -} -``` - -#### Example: Evaluation with Model Configuration - -```json -{ - "jsonrpc": "2.0", - "method": "evaluate", - "params": { - "tool": "chat", - "input": {"message": "Hello"}, - "model": { - "main_model": { - "provider": "openai", - "model": "gpt-4", - "api_key": "sk-main-key" - }, - "mini_model": { - "provider": "openai", - "model": "gpt-4-mini", - "api_key": "sk-mini-key" - }, - "nano_model": { - "provider": "groq", - "model": "llama-3.1-8b-instant", - "api_key": "gsk-nano-key" - } - } - } -} -``` - -### Dynamic LLM Configuration - -The server supports runtime LLM configuration via the `configure_llm` JSON-RPC method: - -```json -{ - "jsonrpc": "2.0", - "method": "configure_llm", - "params": { - "provider": "openai|groq|openrouter|litellm", - "apiKey": "your-api-key", - "endpoint": "endpoint-url-for-litellm", - "models": { - "main": "main-model-name", - "mini": "mini-model-name", - "nano": "nano-model-name" - }, - "partial": false - }, - "id": "config-request-id" -} -``` - -### Configuration - -All configuration is managed through environment variables and `src/config.js`. Key settings: -- Server port and host -- OpenAI API configuration -- RPC timeouts -- Logging levels and directories -- Maximum concurrent evaluations \ No newline at end of file diff --git a/eval-server/nodejs/clients/1233ae25-9f9e-4f77-924d-865f7d615cef.yaml b/eval-server/nodejs/clients/1233ae25-9f9e-4f77-924d-865f7d615cef.yaml deleted file mode 100644 index f5b865f5b5..0000000000 --- a/eval-server/nodejs/clients/1233ae25-9f9e-4f77-924d-865f7d615cef.yaml +++ /dev/null @@ -1,12 +0,0 @@ -client: - id: 1233ae25-9f9e-4f77-924d-865f7d615cef - name: DevTools Client 1233ae25 - secret_key: hello - description: Auto-generated DevTools evaluation client -settings: - max_concurrent_evaluations: 3 - default_timeout: 45000 - retry_policy: - max_retries: 2 - backoff_multiplier: 2 - initial_delay: 1000 diff --git a/eval-server/nodejs/docs/CLIENT_SETUP.md b/eval-server/nodejs/docs/CLIENT_SETUP.md deleted file mode 100644 index 53502ae074..0000000000 --- a/eval-server/nodejs/docs/CLIENT_SETUP.md +++ /dev/null @@ -1,445 +0,0 @@ -# Client Setup Guide - -## Overview - -This guide explains how to set up a new evaluation client to connect to the evaluation server. Clients can be any application that implements the WebSocket evaluation protocol, such as Chrome DevTools or custom test agents. - -## Prerequisites - -- WebSocket client library -- JSON-RPC 2.0 implementation -- UUID v4 generator -- Tools/agents to execute evaluations - -## Setup Steps - -### 1. Generate Client ID - -Generate a unique UUID v4 for your client: - -```javascript -// JavaScript example -import { v4 as uuidv4 } from 'uuid'; -const clientId = uuidv4(); // e.g., "550e8400-e29b-41d4-a716-446655440000" -``` - -Store this ID persistently - it will be used for all connections. - -### 2. Request YAML Configuration - -Contact the evaluation server administrator to: -1. Create a YAML evaluation file for your client ID -2. Optionally set up a secret key for authentication -3. Configure appropriate evaluations for your client - -Example request: -``` -Client ID: 550e8400-e29b-41d4-a716-446655440000 -Client Name: Chrome DevTools Production -Tools Available: extract_schema_data, research_agent, action_agent -Purpose: Automated regression testing -``` - -### 3. Implement WebSocket Connection - -```javascript -class EvaluationClient { - constructor(serverUrl, clientId, secretKey) { - this.serverUrl = serverUrl; - this.clientId = clientId; - this.secretKey = secretKey; - this.ws = null; - } - - connect() { - this.ws = new WebSocket(this.serverUrl); - - this.ws.onopen = () => { - console.log('Connected to evaluation server'); - }; - - this.ws.onmessage = (event) => { - this.handleMessage(JSON.parse(event.data)); - }; - - this.ws.onerror = (error) => { - console.error('WebSocket error:', error); - }; - } -} -``` - -### 4. Implement Protocol Messages - -#### Handle Welcome Message -```javascript -handleMessage(message) { - switch (message.type) { - case 'welcome': - // Server is ready, send registration - this.register(); - break; - - case 'registration_ack': - if (message.status === 'accepted') { - console.log(`Registered! ${message.evaluationsCount} evaluations assigned`); - this.sendReady(); - } else { - console.error('Registration rejected:', message.reason); - } - break; - - default: - // Handle other messages... - } -} -``` - -#### Send Registration -```javascript -register() { - this.send({ - type: 'register', - clientId: this.clientId, - secretKey: this.secretKey, // Optional - capabilities: { - tools: ['extract_schema_data', 'research_agent'], - maxConcurrency: 3, - version: '1.0.0' - } - }); -} -``` - -#### Send Ready Signal -```javascript -sendReady() { - this.send({ - type: 'ready', - timestamp: new Date().toISOString() - }); -} -``` - -### 5. Implement RPC Handler - -```javascript -handleMessage(message) { - // ... existing code ... - - // Handle JSON-RPC requests - if (message.jsonrpc === '2.0' && message.method) { - this.handleRpcRequest(message); - } -} - -async handleRpcRequest(request) { - if (request.method === 'evaluate') { - try { - const result = await this.executeEvaluation(request.params); - - this.send({ - jsonrpc: '2.0', - result: { - status: 'success', - output: result.output, - executionTime: result.duration, - toolCalls: result.toolCalls, - metadata: result.metadata - }, - id: request.id - }); - } catch (error) { - this.send({ - jsonrpc: '2.0', - error: { - code: -32000, - message: error.message, - data: { - tool: request.params.tool, - error: error.toString(), - timestamp: new Date().toISOString() - } - }, - id: request.id - }); - } - } -} -``` - -### 6. Implement Tool Execution - -```javascript -async executeEvaluation(params) { - const startTime = Date.now(); - - // Send status update - this.send({ - type: 'status', - evaluationId: params.evaluationId, - status: 'running', - progress: 0.1, - message: 'Starting evaluation...' - }); - - // Execute the appropriate tool - let result; - switch (params.tool) { - case 'extract_schema_data': - result = await this.extractSchema(params.url, params.input); - break; - - case 'research_agent': - result = await this.runResearchAgent(params.url, params.input); - break; - - default: - throw new Error(`Unknown tool: ${params.tool}`); - } - - const executionTime = Date.now() - startTime; - - return { - output: result, - duration: executionTime, - toolCalls: [{ - tool: params.tool, - timestamp: new Date().toISOString(), - duration: executionTime, - status: 'success' - }], - metadata: { - url: params.url, - toolVersion: '1.0.0' - } - }; -} -``` - -## Chrome DevTools Integration - -For Chrome DevTools specifically: - -### 1. Update EvaluationConfig - -```typescript -// In EvaluationConfig.ts -interface EvaluationConfiguration { - enabled: boolean; - endpoint: string; - secretKey?: string; - clientId?: string; // Add client ID field -} - -// Generate and store client ID -function ensureClientId(): string { - let clientId = localStorage.getItem('ai_chat_evaluation_client_id'); - if (!clientId) { - clientId = generateUUID(); - localStorage.setItem('ai_chat_evaluation_client_id', clientId); - } - return clientId; -} -``` - -### 2. Create Evaluation Agent - -```typescript -// EvaluationAgent.ts -import { WebSocketRPCClient } from '../common/WebSocketRPCClient.js'; -import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; - -export class EvaluationAgent { - private client: WebSocketRPCClient; - private clientId: string; - - constructor(config: EvaluationConfiguration) { - this.clientId = config.clientId || ensureClientId(); - this.client = new WebSocketRPCClient({ - endpoint: config.endpoint, - secretKey: config.secretKey - }); - - this.setupHandlers(); - } - - private setupHandlers(): void { - this.client.on('connected', () => { - this.register(); - }); - - // Handle RPC requests - this.client.on('rpc-request', async (request) => { - if (request.method === 'evaluate') { - const result = await this.handleEvaluation(request.params); - return result; - } - }); - } - - private async handleEvaluation(params: any): Promise { - const tool = ToolRegistry.getRegisteredTool(params.tool); - if (!tool) { - throw new Error(`Tool not found: ${params.tool}`); - } - - // Execute tool with params.input - const result = await tool.execute(params.input); - - return { - status: 'success', - output: result, - executionTime: Date.now() - startTime - }; - } -} -``` - -## Testing Your Client - -### 1. Local Testing - -Use the example agent to test your server setup: - -```bash -# In bo-eval-server directory -npm test -``` - -### 2. Connection Test - -```javascript -// Quick connection test -const client = new EvaluationClient( - 'ws://localhost:8080', - 'your-client-id', - 'optional-secret' -); - -client.connect(); - -// Should see: -// Connected to evaluation server -// Registered! X evaluations assigned -``` - -### 3. Manual Evaluation Test - -You can trigger evaluations manually through the server's CLI: - -```bash -npm run cli -> run-evaluation your-client-id evaluation-id -``` - -## Troubleshooting - -### Connection Issues - -1. **Check server is running** - ```bash - curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" http://localhost:8080 - ``` - -2. **Verify client ID exists** - - Check `clients/{your-client-id}.yaml` exists on server - - Ensure client ID format is valid UUID v4 - -3. **Authentication failures** - - Verify secret key matches server configuration - - Check for typos in client ID or secret - -### Evaluation Failures - -1. **Tool not found** - - Ensure tool name in YAML matches client capabilities - - Verify tool is registered in your client - -2. **Timeouts** - - Increase timeout in YAML configuration - - Check for infinite loops in tool execution - -3. **Invalid input** - - Validate input against expected schema - - Check for required fields - -## Security Best Practices - -1. **Store credentials securely** - - Never hardcode secret keys - - Use environment variables or secure storage - -2. **Validate inputs** - - Sanitize URLs before navigation - - Validate schemas before execution - -3. **Resource limits** - - Implement timeout handling - - Limit concurrent evaluations - -4. **Use WSS in production** - ```javascript - const client = new EvaluationClient( - 'wss://eval-server.example.com', // Use WSS - clientId, - secretKey - ); - ``` - -## Example: Minimal Client - -```javascript -// minimal-client.js -import WebSocket from 'ws'; - -const CLIENT_ID = 'your-uuid-here'; -const SECRET_KEY = 'your-secret-here'; - -const ws = new WebSocket('ws://localhost:8080'); - -ws.on('open', () => { - console.log('Connected'); -}); - -ws.on('message', async (data) => { - const msg = JSON.parse(data); - - if (msg.type === 'welcome') { - // Register - ws.send(JSON.stringify({ - type: 'register', - clientId: CLIENT_ID, - secretKey: SECRET_KEY, - capabilities: { - tools: ['extract_schema_data'], - maxConcurrency: 1, - version: '1.0.0' - } - })); - } - - if (msg.type === 'registration_ack' && msg.status === 'accepted') { - // Send ready - ws.send(JSON.stringify({ - type: 'ready', - timestamp: new Date().toISOString() - })); - } - - if (msg.jsonrpc && msg.method === 'evaluate') { - // Simple evaluation response - ws.send(JSON.stringify({ - jsonrpc: '2.0', - result: { - status: 'success', - output: { message: 'Evaluation completed' }, - executionTime: 1000 - }, - id: msg.id - })); - } -}); - -ws.on('error', console.error); -``` \ No newline at end of file diff --git a/eval-server/nodejs/docs/PROTOCOL.md b/eval-server/nodejs/docs/PROTOCOL.md deleted file mode 100644 index 694e58a69d..0000000000 --- a/eval-server/nodejs/docs/PROTOCOL.md +++ /dev/null @@ -1,310 +0,0 @@ -# WebSocket Evaluation Protocol - -## Overview - -This document describes the WebSocket communication protocol between evaluation clients (e.g., Chrome DevTools) and the evaluation server. The protocol supports client registration, authentication, and bidirectional evaluation task execution using JSON-RPC 2.0. - -## Connection Flow - -``` -Client Server - | | - |------ WebSocket Connect ------>| - | | - |<----- Welcome Message ---------| - | | - |------ Register Message ------->| - | | - |<----- Registration ACK ---------| - | | - |------ Ready Signal ----------->| - | | - |<===== Evaluation Loop ========>| -``` - -## Message Types - -### 1. Client → Server Messages - -#### 1.1 Registration Message -Sent immediately after receiving the welcome message to register the client with the server. - -```json -{ - "type": "register", - "clientId": "550e8400-e29b-41d4-a716-446655440000", - "secretKey": "optional-secret-key", // Optional field for authentication - "capabilities": { - "tools": ["extract_schema_data", "research_agent", "action_agent"], - "maxConcurrency": 3, - "version": "1.0.0" - } -} -``` - -**Fields:** -- `type`: Must be "register" -- `clientId`: UUID v4 format, unique identifier for the client -- `secretKey`: Optional authentication key -- `capabilities`: Object describing client capabilities - - `tools`: Array of tool names the client can execute - - `maxConcurrency`: Maximum number of concurrent evaluations - - `version`: Client version string - -#### 1.2 Ready Signal -Indicates the client is ready to receive evaluation tasks. - -```json -{ - "type": "ready", - "timestamp": "2024-01-01T00:00:00Z" -} -``` - -#### 1.3 Status Update -Provides progress updates for running evaluations. - -```json -{ - "type": "status", - "evaluationId": "eval-123", - "status": "running" | "completed" | "failed", - "progress": 0.5, // Optional, value between 0 and 1 - "message": "Processing page content..." // Optional status message -} -``` - -#### 1.4 Heartbeat (Ping) -Keep-alive message to maintain connection. - -```json -{ - "type": "ping", - "timestamp": "2024-01-01T00:00:00Z" -} -``` - -### 2. Server → Client Messages - -#### 2.1 Welcome Message -Sent immediately after WebSocket connection is established. - -```json -{ - "type": "welcome", - "serverId": "server-001", - "version": "1.0.0", - "timestamp": "2024-01-01T00:00:00Z" -} -``` - -#### 2.2 Registration Acknowledgment -Response to client registration. - -```json -{ - "type": "registration_ack", - "clientId": "550e8400-e29b-41d4-a716-446655440000", - "status": "accepted" | "rejected", - "message": "Client registered successfully", - "evaluationsCount": 5, // Number of evaluations assigned to this client - "reason": "Invalid secret key" // Only present if status is "rejected" -} -``` - -#### 2.3 Heartbeat Response (Pong) -Response to client ping. - -```json -{ - "type": "pong", - "timestamp": "2024-01-01T00:00:00Z" -} -``` - -## JSON-RPC 2.0 Evaluation Protocol - -The evaluation tasks are sent using JSON-RPC 2.0 protocol over the WebSocket connection. - -### 3. Evaluation Request (Server → Client) - -#### 3.1 Evaluate Method -Requests the client to execute an evaluation task. - -```json -{ - "jsonrpc": "2.0", - "method": "evaluate", - "params": { - "evaluationId": "wikipedia-chrome-devtools-001", - "name": "Extract Chrome DevTools Wikipedia Article", - "url": "https://en.wikipedia.org/wiki/Chrome_DevTools", - "tool": "extract_schema_data", - "input": { - "schema": { - "type": "object", - "properties": { - "title": {"type": "string"}, - "summary": {"type": "string"}, - "tableOfContents": { - "type": "array", - "items": {"type": "string"} - } - } - } - }, - "timeout": 30000, // Timeout in milliseconds - "metadata": { - "tags": ["schema-extraction", "wikipedia"], - "retries": 2, - "priority": "normal" - } - }, - "id": "rpc-001" -} -``` - -**Parameters:** -- `evaluationId`: Unique identifier for this evaluation (from YAML definition) -- `name`: Human-readable name of the evaluation -- `url`: Target URL for the evaluation -- `tool`: Name of the tool to execute -- `input`: Tool-specific input parameters -- `timeout`: Maximum execution time in milliseconds -- `metadata`: Additional evaluation metadata - -### 4. Evaluation Response (Client → Server) - -#### 4.1 Success Response -Sent when evaluation completes successfully. - -```json -{ - "jsonrpc": "2.0", - "result": { - "status": "success", - "output": { - "title": "Chrome DevTools", - "summary": "Chrome DevTools is a set of web developer tools built directly into the Google Chrome browser.", - "tableOfContents": [ - "Overview", - "Features", - "History", - "Usage" - ] - }, - "executionTime": 2500, // Total execution time in milliseconds - "toolCalls": [ - { - "tool": "extract_schema_data", - "timestamp": "2024-01-01T00:00:00Z", - "duration": 2400, - "status": "success" - } - ], - "metadata": { - "pageLoadTime": 800, - "extractionTime": 1700, - "retryCount": 0 - } - }, - "id": "rpc-001" -} -``` - -#### 4.2 Error Response -Sent when evaluation fails. - -```json -{ - "jsonrpc": "2.0", - "error": { - "code": -32000, - "message": "Tool execution failed", - "data": { - "tool": "extract_schema_data", - "error": "Page load timeout after 30000ms", - "url": "https://en.wikipedia.org/wiki/Chrome_DevTools", - "timestamp": "2024-01-01T00:00:00Z", - "stackTrace": "Error: Timeout...\n at PageLoader.load..." // Optional - } - }, - "id": "rpc-001" -} -``` - -## Error Codes - -Standard JSON-RPC 2.0 error codes: -- `-32700`: Parse error - Invalid JSON was received -- `-32600`: Invalid request - JSON is not a valid request object -- `-32601`: Method not found - Method does not exist -- `-32602`: Invalid params - Invalid method parameters -- `-32603`: Internal error - Internal JSON-RPC error - -Custom error codes for evaluation: -- `-32000`: Tool execution error - Tool failed during execution -- `-32001`: Timeout error - Evaluation exceeded timeout -- `-32002`: Authentication error - Invalid or missing credentials -- `-32003`: Rate limit exceeded - Too many requests -- `-32004`: Invalid tool - Requested tool not available -- `-32005`: Resource error - Unable to access required resources - -## Connection Management - -### Reconnection -- Clients should implement automatic reconnection with exponential backoff -- On reconnection, clients must re-register with the same clientId -- Server maintains evaluation state across reconnections - -### Timeouts -- Default connection timeout: 60 seconds -- Ping interval: 30 seconds -- Evaluation timeout: Specified per evaluation in YAML - -### Rate Limiting -- Server may implement rate limiting per client -- Rate limit errors use code `-32003` -- Clients should respect rate limit headers in error responses - -## Security Considerations - -1. **Authentication**: Clients may use optional secret keys for authentication -2. **Transport Security**: Production deployments should use WSS (WebSocket Secure) -3. **Input Validation**: All inputs should be validated against schemas -4. **Resource Limits**: Enforce timeouts and memory limits for evaluations - -## Examples - -### Complete Flow Example - -1. **Client connects and registers:** -```json -// Client → Server -{"type": "register", "clientId": "550e8400-e29b-41d4-a716-446655440000", "capabilities": {"tools": ["extract_schema_data"], "maxConcurrency": 3, "version": "1.0.0"}} - -// Server → Client -{"type": "registration_ack", "clientId": "550e8400-e29b-41d4-a716-446655440000", "status": "accepted", "message": "Client registered successfully", "evaluationsCount": 2} -``` - -2. **Client signals ready:** -```json -// Client → Server -{"type": "ready", "timestamp": "2024-01-01T00:00:00Z"} -``` - -3. **Server sends evaluation:** -```json -// Server → Client -{"jsonrpc": "2.0", "method": "evaluate", "params": {"evaluationId": "test-001", "url": "https://example.com", "tool": "extract_schema_data", "input": {"schema": {"type": "object", "properties": {"title": {"type": "string"}}}}, "timeout": 30000}, "id": "rpc-001"} -``` - -4. **Client returns result:** -```json -// Client → Server -{"jsonrpc": "2.0", "result": {"status": "success", "output": {"title": "Example Domain"}, "executionTime": 1500}, "id": "rpc-001"} -``` - -## Version History - -- **1.0.0** (2024-01-01): Initial protocol version \ No newline at end of file diff --git a/eval-server/nodejs/docs/TRIGGERING_EVALUATIONS.md b/eval-server/nodejs/docs/TRIGGERING_EVALUATIONS.md deleted file mode 100644 index 4dd007804d..0000000000 --- a/eval-server/nodejs/docs/TRIGGERING_EVALUATIONS.md +++ /dev/null @@ -1,306 +0,0 @@ -# How to Trigger Evaluations - -This guide explains all the different ways to trigger evaluations in the system. - -## Prerequisites - -1. **Server Running**: Make sure the evaluation server is running: - ```bash - npm start - ``` - -2. **Client Connected**: A DevTools client must be connected and ready. You'll see logs like: - ``` - [info]: Client registered successfully {"clientId":"550e8400...","capabilities":"extract_schema_data, research_agent"} - [info]: Client ready for evaluations {"clientId":"550e8400..."} - ``` - -## Method 1: Interactive CLI - -Start the interactive CLI: -```bash -npm run cli -``` - -### Available Commands - -#### List Clients and Evaluations -```bash -eval-server> clients -``` -This shows all registered clients and their available evaluations with current status. - -#### Run Specific Evaluation -```bash -eval-server> run -``` -Example: -```bash -eval-server> run 550e8400-e29b-41d4-a716-446655440000 wikipedia-chrome-devtools-001 -``` - -#### Run All Evaluations for a Client -```bash -eval-server> run-all -``` -Example: -```bash -eval-server> run-all 550e8400-e29b-41d4-a716-446655440000 -``` - -#### Check Status -```bash -eval-server> status -``` -Shows server status, connected clients, and active evaluations. - -#### Get Help -```bash -eval-server> help -``` - -## Method 2: HTTP API - -The server also exposes an HTTP API on port 8081. - -### Get Server Status -```bash -curl http://localhost:8081/status -``` - -### List All Clients -```bash -curl http://localhost:8081/clients -``` - -### Get Client Evaluations -```bash -curl "http://localhost:8081/clients/:id/evaluations?id=550e8400-e29b-41d4-a716-446655440000" -``` - -### Trigger Specific Evaluation -```bash -curl -X POST http://localhost:8081/evaluate \\ - -H "Content-Type: application/json" \\ - -d '{ - "clientId": "550e8400-e29b-41d4-a716-446655440000", - "evaluationId": "wikipedia-chrome-devtools-001" - }' -``` - -### Trigger All Evaluations for a Client -```bash -curl -X POST http://localhost:8081/evaluate \\ - -H "Content-Type: application/json" \\ - -d '{ - "clientId": "550e8400-e29b-41d4-a716-446655440000", - "runAll": true - }' -``` - - -## Method 3: Programmatic Integration - -You can integrate the evaluation system into your own applications: - -### Node.js Example -```javascript -import { EvaluationServer } from './src/server.js'; - -const server = new EvaluationServer(); -server.start(); - -// Wait for client to connect -setTimeout(async () => { - const clientId = '550e8400-e29b-41d4-a716-446655440000'; - const evaluationId = 'wikipedia-chrome-devtools-001'; - - // Get client connection - const connection = server.connectedAgents.get(clientId); - if (connection && connection.ready) { - // Get evaluation - const evaluation = server.getClientManager() - .getClientEvaluations(clientId) - .find(e => e.id === evaluationId); - - if (evaluation) { - // Execute evaluation - await server.executeEvaluation(connection, evaluation); - console.log('Evaluation completed!'); - } - } -}, 5000); -``` - -### Python Example (using HTTP API) -```python -import requests -import json - -def trigger_evaluation(client_id, evaluation_id): - response = requests.post('http://localhost:8081/evaluate', - headers={'Content-Type': 'application/json'}, - json={ - 'clientId': client_id, - 'evaluationId': evaluation_id - }) - - if response.status_code == 200: - return response.json() - else: - raise Exception(f"Failed to trigger evaluation: {response.text}") - -# Example usage -result = trigger_evaluation( - '550e8400-e29b-41d4-a716-446655440000', - 'wikipedia-chrome-devtools-001' -) -print(json.dumps(result, indent=2)) -``` - -## Method 4: Webhook Integration - -You can set up webhooks to trigger evaluations from external systems: - -### GitHub Actions Example -```yaml -name: Run Evaluations -on: - schedule: - - cron: '0 9 * * *' # Daily at 9 AM - workflow_dispatch: # Manual trigger - -jobs: - evaluate: - runs-on: ubuntu-latest - steps: - - name: Trigger Evaluation - run: | - curl -X POST ${{ secrets.EVAL_SERVER_URL }}/evaluate \\ - -H "Content-Type: application/json" \\ - -d '{ - "clientId": "${{ secrets.CLIENT_ID }}", - "runAll": true - }' -``` - -### Slack Bot Example -```javascript -// Slack bot command: /eval wikipedia -app.command('/eval', async ({ command, ack, respond }) => { - await ack(); - - const evaluationId = command.text.trim(); - const clientId = process.env.DEFAULT_CLIENT_ID; - - try { - const response = await fetch('http://localhost:8081/evaluate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ clientId, evaluationId }) - }); - - const result = await response.json(); - await respond(`✅ Evaluation '${evaluationId}' completed successfully!`); - } catch (error) { - await respond(`❌ Evaluation failed: ${error.message}`); - } -}); -``` - -## Monitoring Evaluation Results - -### Real-time Logs -Monitor the server logs to see evaluation progress: -```bash -tail -f logs/combined.log -``` - -### Status Checking -Check evaluation status via API: -```bash -# Get all evaluations for a client -curl "http://localhost:8081/clients/:id/evaluations?id=CLIENT_ID" - -# Check server status -curl http://localhost:8081/status -``` - -### Log Files -Evaluation results are logged to: -- `logs/combined.log` - All logs -- `logs/error.log` - Error logs only - -## Troubleshooting - -### Client Not Connected -``` -❌ Client 'CLIENT_ID' is not connected or not ready -``` -**Solutions:** -1. Make sure DevTools is running and connected -2. Check that the client ID matches -3. Verify the WebSocket connection is working - -### Evaluation Not Found -``` -❌ Evaluation 'EVAL_ID' not found for client 'CLIENT_ID' -``` -**Solutions:** -1. Check the YAML file for the correct evaluation ID -2. Ensure the evaluation is enabled (`enabled: true`) -3. Reload the server if you changed the YAML file - -### Tool Not Available -``` -Tool execution failed: Tool not found: tool_name -``` -**Solutions:** -1. Verify the tool is registered in DevTools -2. Check that the tool name matches exactly -3. Ensure DevTools has the required capabilities - -### Connection Timeout -``` -WebSocket connection failed -``` -**Solutions:** -1. Check if the server is running on the correct port -2. Verify firewall settings -3. Check network connectivity - -## Best Practices - -1. **Start Simple**: Begin with manual evaluations before setting up automation -2. **Monitor Logs**: Always monitor logs when running evaluations -3. **Test Connections**: Use the `status` command to verify everything is connected -4. **Gradual Rollout**: Test individual evaluations before running batch operations -5. **Error Handling**: Implement proper error handling in automated systems -6. **Rate Limiting**: Don't run too many evaluations simultaneously - -## Example Workflow - -Here's a typical workflow for triggering evaluations: - -```bash -# 1. Start the server -npm start - -# 2. In another terminal, start the CLI -npm run cli - -# 3. Check status and clients -eval-server> status -eval-server> clients - -# 4. Run a specific evaluation -eval-server> run 550e8400-e29b-41d4-a716-446655440000 wikipedia-chrome-devtools-001 - -# 5. Check results in logs -# (Monitor the server logs for detailed results) - -# 6. Run all evaluations if needed -eval-server> run-all 550e8400-e29b-41d4-a716-446655440000 -``` - -This comprehensive guide covers all the ways to trigger and monitor evaluations in your system! \ No newline at end of file diff --git a/eval-server/nodejs/docs/YAML_SCHEMA.md b/eval-server/nodejs/docs/YAML_SCHEMA.md deleted file mode 100644 index ea15dcd893..0000000000 --- a/eval-server/nodejs/docs/YAML_SCHEMA.md +++ /dev/null @@ -1,315 +0,0 @@ -# YAML Evaluation Schema Documentation - -## Overview - -This document describes the YAML schema used to define evaluations for each client. Each client has a dedicated YAML file stored in the `clients/` directory, named after their client ID. - -## File Location - -``` -bo-eval-server/ -└── clients/ - ├── 550e8400-e29b-41d4-a716-446655440000.yaml - ├── 771f9500-f39c-52e5-b827-557766551111.yaml - └── ... -``` - -## Schema Structure - -### Root Level - -```yaml -# Client identification and authentication -client: - id: "550e8400-e29b-41d4-a716-446655440000" # Required: UUID v4 - name: "Chrome DevTools Agent" # Required: Human-readable name - secret_key: "optional-secret-key" # Optional: Authentication key - description: "Production DevTools instance" # Optional: Client description - -# Client-specific settings -settings: - max_concurrent_evaluations: 3 # Maximum parallel evaluations - default_timeout: 30000 # Default timeout in milliseconds - retry_policy: - max_retries: 2 # Maximum retry attempts - backoff_multiplier: 2 # Exponential backoff multiplier - initial_delay: 1000 # Initial retry delay in ms - -# List of evaluations assigned to this client -evaluations: - - id: "eval-001" - # ... evaluation definition - - id: "eval-002" - # ... evaluation definition -``` - -### Evaluation Definition - -Each evaluation in the `evaluations` array follows this structure: - -```yaml -- id: "wikipedia-chrome-devtools-001" # Required: Unique evaluation ID - name: "Extract Chrome DevTools Wikipedia" # Required: Display name - description: "Extract structured data" # Optional: Detailed description - enabled: true # Optional: Enable/disable (default: true) - - # Target configuration - target: - url: "https://en.wikipedia.org/wiki/Chrome_DevTools" # Required: Target URL - wait_for: "networkidle" # Optional: Wait condition (load|domcontentloaded|networkidle) - wait_timeout: 5000 # Optional: Wait timeout in ms - - # Tool configuration - tool: "extract_schema_data" # Required: Tool to execute - timeout: 30000 # Optional: Override default timeout - - # Tool-specific input - input: - schema: # For extract_schema_data tool - type: "object" - properties: - title: - type: "string" - summary: - type: "string" - - - # Validation configuration - validation: - type: "llm-judge" # llm-judge|snapshot|hybrid - - # For llm-judge validation - llm_judge: - model: "gpt-4o-mini" # LLM model to use - temperature: 0.3 # Model temperature - criteria: # Evaluation criteria - - "Title should be accurately extracted" - - "Summary should be comprehensive" - - "All required fields should be present" - - # Visual verification settings - visual_verification: - enabled: true - capture_before: true # Screenshot before tool execution - capture_after: true # Screenshot after tool execution - prompts: # Custom verification prompts - - "Verify the title matches the page header" - - # For snapshot validation - snapshot: - structure_only: false # Compare structure only - exclude_paths: # Paths to exclude from comparison - - "timestamp" - - "random_id" - sanitizers: # Value sanitization rules - - path: "date" - pattern: "\\d{4}-\\d{2}-\\d{2}" - replacement: "YYYY-MM-DD" - - # For hybrid validation (both llm-judge and snapshot) - hybrid: - weight_llm: 0.7 # Weight for LLM score - weight_snapshot: 0.3 # Weight for snapshot score - - # Metadata and tags - metadata: - tags: # Categorization tags - - "schema-extraction" - - "wikipedia" - - "regression" - priority: "normal" # low|normal|high - owner: "team-browser" # Responsible team/person - created: "2024-01-01" # Creation date - modified: "2024-01-15" # Last modification date -``` - -## Tool-Specific Input Schemas - -### extract_schema_data - -```yaml -input: - schema: # JSON Schema for extraction - type: "object" - properties: - title: - type: "string" - items: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - price: - type: "number" -``` - -### research_agent - -```yaml -input: - query: "Research the latest AI developments" # Research query - max_iterations: 5 # Maximum agent iterations - include_sources: true # Include source URLs - depth: "comprehensive" # shallow|moderate|comprehensive -``` - -### action_agent - -```yaml -input: - task: "Fill out the contact form" # Task description - form_data: # Data to use - name: "Test User" - email: "test@example.com" - verify_completion: true # Verify task completion -``` - -### web_task_agent - -```yaml -input: - instructions: | # Multi-line instructions - 1. Navigate to the products page - 2. Search for "laptop" - 3. Filter by price < $1000 - 4. Extract the first 5 results - expected_outcome: "List of laptops under $1000" - max_steps: 10 # Maximum action steps -``` - -## Complete Example - -```yaml -client: - id: "550e8400-e29b-41d4-a716-446655440000" - name: "Chrome DevTools Production Agent" - secret_key: "sk-prod-abc123" - description: "Production DevTools instance for continuous evaluation" - -settings: - max_concurrent_evaluations: 5 - default_timeout: 45000 - retry_policy: - max_retries: 3 - backoff_multiplier: 2 - initial_delay: 2000 - -evaluations: - # Schema extraction evaluation - - id: "schema-extract-wiki-001" - name: "Wikipedia Chrome DevTools Schema Extraction" - description: "Test schema extraction on Wikipedia article" - enabled: true - - target: - url: "https://en.wikipedia.org/wiki/Chrome_DevTools" - wait_for: "networkidle" - wait_timeout: 5000 - - tool: "extract_schema_data" - timeout: 30000 - - input: - schema: - type: "object" - properties: - title: - type: "string" - summary: - type: "string" - features: - type: "array" - items: - type: "string" - lastModified: - type: "string" - - - validation: - type: "hybrid" - llm_judge: - model: "gpt-4o" - criteria: - - "All schema fields must be populated" - - "Summary should be at least 100 characters" - - "Features should contain at least 5 items" - snapshot: - exclude_paths: - - "lastModified" - hybrid: - weight_llm: 0.6 - weight_snapshot: 0.4 - - metadata: - tags: ["schema", "wikipedia", "daily"] - priority: "high" - owner: "qa-team" - - # Research agent evaluation - - id: "research-agent-news-001" - name: "Research Latest Tech News" - description: "Test research agent on current tech news" - enabled: true - - target: - url: "https://news.ycombinator.com" - - tool: "research_agent" - timeout: 60000 - - input: - query: "What are the top 3 technology stories today?" - max_iterations: 5 - include_sources: true - depth: "moderate" - - - validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Response includes 3 distinct technology stories" - - "Each story has a clear summary" - - "Sources are provided for each story" - - "Information is current (from today)" - - metadata: - tags: ["research", "news", "tech"] - priority: "normal" -``` - -## Validation Rules - -1. **Client ID**: Must be valid UUID v4 format -2. **Evaluation IDs**: Must be unique within the file -3. **Tool names**: Must match registered tools in the client -4. **URLs**: Must be valid HTTP/HTTPS URLs -5. **Timeouts**: Must be positive integers (milliseconds) - -## YAML Best Practices - -1. Use meaningful IDs that describe the evaluation -2. Group related evaluations together -3. Use tags consistently for categorization -4. Document complex input schemas with comments -5. Keep validation criteria specific and measurable -6. Use anchors and aliases for repeated configurations: - -```yaml -# Define anchor -defaults: &defaults - timeout: 30000 - retry_policy: - max_retries: 2 - -# Use alias -evaluations: - - id: "eval-001" - <<: *defaults # Inherits timeout and retry_policy - name: "Test 1" - # ... -``` \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/a11y-001.yaml b/eval-server/nodejs/evals/action-agent/a11y-001.yaml deleted file mode 100644 index 7c7947a8e8..0000000000 --- a/eval-server/nodejs/evals/action-agent/a11y-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Accessibility action test -id: "a11y-001" -name: "Click Using ARIA Label" -description: "Test clicking an element identified primarily by ARIA attributes" -enabled: true - -target: - url: "https://www.w3.org/WAI/ARIA/apg/patterns/button/examples/button/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click the button with aria-label \"Print Page\"" - reasoning: "Testing action selection using accessibility attributes" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Used accessibility tree to find elements" - - "Correctly identified element by ARIA label" - - "Successfully clicked the target button" - - "Demonstrated understanding of accessibility attributes" - - "No reliance on visual appearance alone" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the Print Page button was successfully clicked" - - "Check if any print dialog or print preview appeared" - - "Confirm the button showed visual feedback (pressed state)" - - "Ensure the action was performed on the correct accessibility-labeled element" - -metadata: - tags: ["action", "accessibility", "aria", "click", "a11y"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/accordion-001.yaml b/eval-server/nodejs/evals/action-agent/accordion-001.yaml deleted file mode 100644 index dae142d03c..0000000000 --- a/eval-server/nodejs/evals/action-agent/accordion-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Accordion expansion test -id: "accordion-001" -name: "Expand Accordion Section" -description: "Test clicking to expand an accordion panel" -enabled: true - -target: - url: "https://jqueryui.com/accordion/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click to expand the \"Section 2\" accordion panel" - reasoning: "Testing accordion expand/collapse interaction" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Section 2 accordion header" - - "Successfully clicked to expand the section" - - "Section 2 content became visible" - - "Other sections collapsed appropriately" - - "Accordion animation completed smoothly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify Section 2 is now expanded and content visible" - - "Check if other accordion sections collapsed" - - "Confirm the expansion animation completed" - - "Ensure Section 2 header shows expanded state" - -metadata: - tags: ["action", "accordion", "expand", "collapse", "ui"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-a11y-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-a11y-001.yaml deleted file mode 100644 index 9526551573..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-a11y-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Accessibility action test -id: "action-agent-a11y-001" -name: "Click Using ARIA Label" -description: "Test clicking an element identified primarily by ARIA attributes" -enabled: true - -target: - url: "https://www.w3.org/WAI/ARIA/apg/patterns/button/examples/button/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click the button with aria-label \"Print Page\"" - reasoning: "Testing action selection using accessibility attributes" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Used accessibility tree to find elements" - - "Correctly identified element by ARIA label" - - "Successfully clicked the target button" - - "Demonstrated understanding of accessibility attributes" - - "No reliance on visual appearance alone" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the Print Page button was successfully clicked" - - "Check if any print dialog or print preview appeared" - - "Confirm the button showed visual feedback (pressed state)" - - "Ensure the action was performed on the correct accessibility-labeled element" - -metadata: - tags: ["action", "accessibility", "aria", "click", "a11y"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-accordion-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-accordion-001.yaml deleted file mode 100644 index f2df343052..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-accordion-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Accordion expansion test -id: "action-agent-accordion-001" -name: "Expand Accordion Section" -description: "Test clicking to expand an accordion panel" -enabled: true - -target: - url: "https://jqueryui.com/accordion/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click to expand the \"Section 2\" accordion panel" - reasoning: "Testing accordion expand/collapse interaction" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Section 2 accordion header" - - "Successfully clicked to expand the section" - - "Section 2 content became visible" - - "Other sections collapsed appropriately" - - "Accordion animation completed smoothly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify Section 2 is now expanded and content visible" - - "Check if other accordion sections collapsed" - - "Confirm the expansion animation completed" - - "Ensure Section 2 header shows expanded state" - -metadata: - tags: ["action", "accordion", "expand", "collapse", "ui"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-autocomplete-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-autocomplete-001.yaml deleted file mode 100644 index c22bfc737c..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-autocomplete-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Autocomplete search test -id: "action-agent-autocomplete-001" -name: "Use Autocomplete Search" -description: "Test typing in autocomplete field and selecting from suggestions" -enabled: true - -target: - url: "https://jqueryui.com/autocomplete/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Type \"Java\" in the autocomplete field and select \"JavaScript\" from suggestions" - reasoning: "Testing autocomplete/typeahead interaction patterns" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the autocomplete input field" - - "Typed \"Java\" to trigger suggestions" - - "Autocomplete dropdown appeared with suggestions" - - "Selected \"JavaScript\" from the suggestion list" - - "Input field shows the selected value" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify \"JavaScript\" appears in the input field" - - "Check if autocomplete suggestions appeared" - - "Confirm the correct suggestion was selected" - - "Ensure dropdown closed after selection" - -metadata: - tags: ["action", "autocomplete", "typeahead", "search", "suggestions"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-checkbox-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-checkbox-001.yaml deleted file mode 100644 index b76f307200..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-checkbox-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Checkbox/radio button test -id: "action-agent-checkbox-001" -name: "Toggle Newsletter Checkbox" -description: "Test clicking checkbox elements for form options" -enabled: true - -target: - url: "https://www.w3schools.com/html/tryit.asp?filename=tryhtml_checkbox" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Click the checkbox labeled \"I have a bike\" to check it" - reasoning: "Testing interaction with checkbox form elements" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Identified the correct checkbox among multiple options" - - "Used click action on the checkbox element" - - "Checkbox state changed from unchecked to checked" - - "Handled the iframe structure if present" - - "No errors with form element interaction" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify the checkbox state changed from unchecked to checked" - - "Confirm the \"I have a bike\" checkbox now shows a checkmark" - - "Verify the checkbox visual indicator (checkmark) is clearly visible" - - "Ensure no other checkboxes were accidentally modified" - -metadata: - tags: ["action", "checkbox", "form", "w3schools", "input"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-checkbox-002.yaml b/eval-server/nodejs/evals/action-agent/action-agent-checkbox-002.yaml deleted file mode 100644 index 0b25fa8195..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-checkbox-002.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Toggle checkbox test - using HTML form test site -id: "action-agent-checkbox-002" -name: "Check Extra Cheese Checkbox" -description: "Test checking a specific checkbox using the check method" -enabled: true - -target: - url: "https://httpbin.org/forms/post" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Find and check the \"Extra Cheese\" checkbox in the Pizza Toppings section" - reasoning: "Testing checkbox interaction functionality using check method" - hint: "Look for the Extra Cheese checkbox and use the check method to select it" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Extra Cheese checkbox in the Pizza Toppings section" - - "Used the check method instead of click for better reliability" - - "Checkbox became checked (if it wasn't already)" - - "No errors occurred during checkbox interaction" - - "Form maintained its structure after checkbox selection" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the Extra Cheese checkbox is now checked (shows checkmark)" - - "Check that the checkbox shows proper visual feedback for checked state" - - "Confirm the form structure remained intact" - - "Ensure the checkbox for Extra Cheese was specifically targeted and checked" - -metadata: - tags: ["action", "checkbox", "check", "form", "httpbin"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-click-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-click-001.yaml deleted file mode 100644 index e9af6cfdf2..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-click-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Basic search interaction test -id: "action-agent-click-001" -name: "Search with Text Entry and Click" -description: "Test entering text in search field and clicking search button" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 90000 - -input: - objective: "Type \"DevTools automation\" in the search box and then click the \"Google Search\" button" - reasoning: "Testing multi-step interaction: text input followed by button click" - hint: "First fill the search input field, then find and click the search button" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Successfully located the search input field" - - "Entered \"DevTools automation\" text in the search box" - - "Located the Google Search button after entering text" - - "Successfully clicked the search button" - - "Search was executed and results page loaded" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify text \"DevTools automation\" was entered in the search field" - - "Check if search results page loaded with relevant results" - - "Confirm the search was executed (URL changed to results page)" - - "Ensure search results are related to \"DevTools automation\"" - -metadata: - tags: ["action", "multi-step", "search", "form-fill", "click", "google", "basic"] - priority: "high" - timeout: 90000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-context-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-context-001.yaml deleted file mode 100644 index 61626977f4..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-context-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Right click context menu test -id: "action-agent-context-001" -name: "Right Click Context Menu" -description: "Test right-clicking to open context menu" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/context_menu" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Right-click on the context menu area to open the context menu" - reasoning: "Testing right-click context menu interaction" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the designated context menu area" - - "Performed right-click action correctly" - - "Context menu appeared with options" - - "Successfully triggered the right-click event" - - "Alert or confirmation appeared as expected" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify right-click was performed on correct area" - - "Check if context menu or alert appeared" - - "Confirm right-click event was properly triggered" - - "Ensure the expected response occurred" - -metadata: - tags: ["action", "context-menu", "right-click", "mouse", "menu"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-datepicker-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-datepicker-001.yaml deleted file mode 100644 index f4abbf7ac3..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-datepicker-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Date picker test -id: "action-agent-datepicker-001" -name: "Select Date from Calendar" -description: "Test clicking date input and selecting a specific date from calendar popup" -enabled: true - -target: - url: "https://jqueryui.com/datepicker/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click the date input field and select March 15, 2024 from the calendar picker" - reasoning: "Testing interaction with calendar popup widgets" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located and clicked the date input field" - - "Calendar popup opened successfully" - - "Navigated to correct month/year if needed" - - "Selected the specific date (March 15, 2024)" - - "Date input field shows the selected date" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the date input field contains the selected date" - - "Check if the calendar widget opened and closed properly" - - "Confirm the correct date was highlighted and selected" - - "Ensure the date format matches expected output" - -metadata: - tags: ["action", "datepicker", "calendar", "form", "popup"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-daterange-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-daterange-001.yaml deleted file mode 100644 index 4581a47205..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-daterange-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Date range picker test -id: "action-agent-daterange-001" -name: "Select Date Range" -description: "Test selecting a date range with start and end dates" -enabled: true - -target: - url: "https://www.daterangepicker.com/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Select a date range from February 1, 2024 to February 28, 2024" - reasoning: "Testing complex date range selection with start and end dates" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Opened the date range picker interface" - - "Selected the start date (February 1, 2024)" - - "Selected the end date (February 28, 2024)" - - "Date range was properly applied" - - "Input field shows the complete date range" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify both start and end dates are displayed in the input" - - "Check if the date range picker shows the selected range" - - "Confirm the format matches expected date range display" - - "Ensure both dates were selected in sequence" - -metadata: - tags: ["action", "daterange", "date-picker", "form", "complex"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-dropdown-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-dropdown-001.yaml deleted file mode 100644 index b37b91c3e3..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-dropdown-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Dropdown selection test -id: "action-agent-dropdown-001" -name: "Select Dropdown Option" -description: "Test selecting an option from a dropdown menu" -enabled: true - -target: - url: "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_select" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Select \"Audi\" from the car brands dropdown menu" - reasoning: "Testing dropdown selection interaction" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the dropdown/select element" - - "Identified the correct option to select" - - "Successfully selected the Audi option" - - "Dropdown value changed to the selected option" - - "Handled select element interaction properly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify the dropdown selection changed" - - "Confirm \"Audi\" is now displayed as the selected option" - - "Check if the dropdown is closed after selection" - - "Verify no other form elements were affected by the selection" - -metadata: - tags: ["action", "dropdown", "select", "form", "w3schools"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-dynamic-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-dynamic-001.yaml deleted file mode 100644 index a4380f33f3..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-dynamic-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Dynamic content interaction test -id: "action-agent-dynamic-001" -name: "Click Dynamic Load Button" -description: "Test clicking a button that loads dynamic content" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/dynamic_loading/1" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 90000 - -input: - objective: "Click the \"Start\" button to trigger dynamic content loading" - reasoning: "Testing interaction with dynamically loaded content" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Found and clicked the Start button" - - "Handled the dynamic loading process" - - "Recognized that content changes after clicking" - - "No timing issues with the dynamic content" - - "Successfully triggered the loading animation" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify dynamic content loaded after clicking Start" - - "Check if loading animation or spinner was displayed" - - "Confirm new content appeared that was previously hidden" - - "Verify the Start button state changed or was replaced after clicking" - -metadata: - tags: ["action", "dynamic", "click", "ajax", "loading"] - priority: "high" - timeout: 90000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-ecommerce-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-ecommerce-001.yaml deleted file mode 100644 index 503c157d37..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-ecommerce-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# E-commerce action test -id: "action-agent-ecommerce-001" -name: "Add Product to Cart" -description: "Test clicking \"Add to Cart\" button on an e-commerce product page" -enabled: true - -target: - url: "https://www.homedepot.com/p/Husky-20-Gal-Professional-Duty-Waterproof-Storage-Container-with-Hinged-Lid-in-Red-249160/313799634" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 180000 - -input: - objective: "Click the \"Add to Cart\" button for this storage container" - reasoning: "Testing e-commerce interaction with product cart functionality" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Add to Cart button on the product page" - - "Successfully clicked the button" - - "Handled any popups or confirmations that appeared" - - "Verified the item was added (cart count changed or confirmation shown)" - - "Dealt with page dynamics after clicking" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify the Add to Cart button was clicked" - - "Check if cart count indicator increased or shows the item was added" - - "Look for any confirmation popup or notification about the item being added" - - "Verify the button state changed (e.g., to \"Added to Cart\" or disabled)" - -metadata: - tags: ["action", "ecommerce", "click", "homedepot", "cart"] - priority: "high" - timeout: 180000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-error-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-error-001.yaml deleted file mode 100644 index 43c95e6d0f..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-error-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Error recovery test -id: "action-agent-error-001" -name: "Handle Missing Element" -description: "Test agent behavior when target element is not found" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click the \"Sign Up\" button" - reasoning: "Testing error handling when element does not exist" - hint: "There is no Sign Up button on Google homepage - agent should handle gracefully" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Attempted to find the requested element" - - "Recognized that the element does not exist" - - "Provided clear error message or explanation" - - "Did not crash or produce confusing output" - - "Suggested alternatives or explained the issue" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the page remains in a stable state despite the missing element" - - "Confirm no error dialogs or broken UI elements appeared" - - "Check that the agent handled the missing element gracefully" - - "Ensure the page was properly analyzed even though the target was not found" - -metadata: - tags: ["action", "error-handling", "missing-element", "recovery", "edge-case"] - priority: "high" - timeout: 60000 - retries: 1 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-filter-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-filter-001.yaml deleted file mode 100644 index 7782999359..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-filter-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Search filter application test -id: "action-agent-filter-001" -name: "Apply Search Filters" -description: "Test applying search filters to modify results" -enabled: true - -target: - url: "https://www.w3schools.com/howto/howto_js_filter_lists.asp" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Type \"Anna\" in the search filter to filter the list" - reasoning: "Testing search filter application" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the search filter input" - - "Typed \"Anna\" in the filter field" - - "List items filtered to show only matching results" - - "Non-matching items were hidden or removed from view" - - "Filter functionality worked as expected" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify search input contains \"Anna\"" - - "Check if list shows only items containing \"Anna\"" - - "Confirm non-matching items are not visible" - - "Ensure filter functionality reduced the visible list items" - -metadata: - tags: ["action", "filter", "search", "list", "dynamic"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-form-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-form-001.yaml deleted file mode 100644 index 61d036f683..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-form-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Form fill action test -id: "action-agent-form-001" -name: "Fill Search Query" -description: "Test filling a search input field with specific text" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Fill the search box with \"Chrome DevTools automation testing\"" - reasoning: "Testing form input capability with a specific search query" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Successfully identified the search input field" - - "Used perform_action with fill method" - - "Correctly filled the field with the specified text" - - "Verified the field accepted the input" - - "No formatting or encoding issues with the text" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to confirm text was entered in the search field" - - "Verify the exact text \"Chrome DevTools automation testing\" is visible" - - "Check if search suggestions or autocomplete dropdown appeared" - - "Ensure no input validation errors are shown" - -metadata: - tags: ["action", "form-fill", "input", "google", "basic"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-hover-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-hover-001.yaml deleted file mode 100644 index ed98fbf6ef..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-hover-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Hover action test -id: "action-agent-hover-001" -name: "Hover to Reveal Menu" -description: "Test hovering over an element to reveal hidden content" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/hovers" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Hover over the first user avatar image to reveal the hidden caption" - reasoning: "Testing hover interaction to reveal dynamic content" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the first user avatar image" - - "Used appropriate hover action method" - - "Successfully triggered the hover state" - - "Hidden caption became visible after hover" - - "Handled mouse interaction correctly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify hover revealed hidden content" - - "Check that caption or overlay appeared over the first avatar" - - "Confirm the hover state is visually active on the image" - - "Verify user information or caption text is now visible" - -metadata: - tags: ["action", "hover", "mouse", "dynamic", "reveal"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-keyboard-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-keyboard-001.yaml deleted file mode 100644 index 6bfceac0b2..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-keyboard-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Keyboard tab navigation test -id: "action-agent-keyboard-001" -name: "Keyboard Tab Navigation" -description: "Test using keyboard navigation to move between elements" -enabled: true - -target: - url: "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Use Tab key to navigate between menu items and Enter to activate" - reasoning: "Testing keyboard-only navigation patterns" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Successfully used keyboard navigation" - - "Tab key moved focus between menu items" - - "Focus indicators were visible during navigation" - - "Enter key activated the focused menu item" - - "Keyboard navigation followed accessibility standards" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify focus indicators are visible on menu items" - - "Check if keyboard navigation moved focus correctly" - - "Confirm Enter key activated the focused item" - - "Ensure accessibility navigation patterns worked" - -metadata: - tags: ["action", "keyboard", "navigation", "accessibility", "focus"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-login-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-login-001.yaml deleted file mode 100644 index 1b705ce8de..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-login-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Login form test -id: "action-agent-login-001" -name: "Fill Login Credentials" -description: "Test filling username and password fields in a login form" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/login" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Fill the username field with \"tomsmith\" and password field with \"SuperSecretPassword!\"" - reasoning: "Testing form fill with multiple fields including password type" - input_data: "tomsmithSuperSecretPassword!" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Identified both username and password fields" - - "Filled username field with correct value" - - "Filled password field with correct value" - - "Handled password field type appropriately" - - "Used the provided input_data XML format correctly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the username field shows \"tomsmith\" entered" - - "Confirm the password field has dots/asterisks indicating password entry" - - "Check that both fields are properly filled before submission" - - "Ensure no validation errors are shown for the filled fields" - -metadata: - tags: ["action", "login", "form-fill", "authentication", "multi-field"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-modal-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-modal-001.yaml deleted file mode 100644 index 1324fee7cf..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-modal-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Modal dialog test -id: "action-agent-modal-001" -name: "Open and Close Modal" -description: "Test opening modal dialog and closing it with X button" -enabled: true - -target: - url: "https://getbootstrap.com/docs/5.0/components/modal/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click to open the modal dialog, then close it using the X button" - reasoning: "Testing modal dialog interaction patterns" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located and clicked the modal trigger button" - - "Modal dialog opened successfully" - - "Modal content was visible and accessible" - - "Found and clicked the close (X) button" - - "Modal closed and page returned to normal state" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify modal opened with visible content" - - "Check if modal overlay appeared correctly" - - "Confirm modal was closed after clicking X" - - "Ensure page background is accessible again" - -metadata: - tags: ["action", "modal", "dialog", "popup", "overlay"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-multiselect-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-multiselect-001.yaml deleted file mode 100644 index fed3f78d27..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-multiselect-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Multi-select dropdown test -id: "action-agent-multiselect-001" -name: "Select Multiple Options" -description: "Test selecting multiple options from a multi-select dropdown" -enabled: true - -target: - url: "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_select_multiple" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Select both \"Volvo\" and \"Audi\" from the multi-select dropdown" - reasoning: "Testing multiple selection in select elements" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the multi-select dropdown element" - - "Successfully selected Volvo option" - - "Successfully selected Audi option" - - "Both options remain selected simultaneously" - - "Used appropriate multi-select interaction method" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify both Volvo and Audi appear selected" - - "Check if both options are highlighted/marked" - - "Confirm multi-select functionality worked correctly" - - "Ensure no other options were accidentally selected" - -metadata: - tags: ["action", "multi-select", "dropdown", "form", "multiple"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-multistep-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-multistep-001.yaml deleted file mode 100644 index 31514dde10..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-multistep-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Multi-step form test -id: "action-agent-multistep-001" -name: "Complete Search and Submit" -description: "Test filling a search form and then clicking the submit button" -enabled: true - -target: - url: "https://www.bing.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Fill the search box with \"automated testing tools\" and then click the search button" - reasoning: "Testing multi-step form interaction combining fill and click actions" - hint: "This requires two actions: first fill the search field, then click the search button" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Recognized this requires multiple actions" - - "First filled the search input correctly" - - "Then located and clicked the search button" - - "Both actions completed successfully in sequence" - - "Search was initiated with the correct query" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the search input contains \"automated testing tools\" text" - - "Confirm the search was submitted and results page loaded" - - "Check that search results are related to the query" - - "Ensure the multi-step action completed fully with both fill and click" - -metadata: - tags: ["action", "multi-step", "form-fill", "click", "bing", "search"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-nav-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-nav-001.yaml deleted file mode 100644 index f49a0cf9b8..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-nav-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Complex navigation test -id: "action-agent-nav-001" -name: "Navigate via Menu Click" -description: "Test clicking navigation menu items to navigate between pages" -enabled: true - -target: - url: "https://www.wikipedia.org" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click on the \"English\" language link to navigate to English Wikipedia" - reasoning: "Testing navigation through link clicks on a multilingual site" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Identified the correct language link among many options" - - "Successfully clicked the English link" - - "Navigation occurred to the English Wikipedia" - - "Used appropriate tools to verify navigation success" - - "Handled the multilingual page structure correctly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify navigation from Wikipedia homepage to English Wikipedia" - - "Check if the page language and content changed to English" - - "Verify the URL changed to en.wikipedia.org" - - "Confirm the English Wikipedia main page is displayed" - -metadata: - tags: ["action", "navigation", "click", "wikipedia", "multilingual"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-radio-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-radio-001.yaml deleted file mode 100644 index 07d6ef8880..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-radio-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Radio button selection test -id: "action-agent-radio-001" -name: "Select Radio Button Option" -description: "Test selecting a specific radio button option using click method" -enabled: true - -target: - url: "https://httpbin.org/forms/post" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Select the \"Medium\" pizza size from the Pizza Size radio button group" - reasoning: "Testing radio button selection functionality" - hint: "Look for the Medium radio button in the Pizza Size section and click it to select" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Medium radio button in the Pizza Size section" - - "Successfully clicked the Medium radio button" - - "Radio button became selected (checked state)" - - "Other radio buttons in the same group became unselected" - - "Form maintained its structure after radio button selection" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the Medium radio button is now selected (shows filled circle)" - - "Check that other pizza size options (Small, Large) are no longer selected" - - "Confirm the form structure remained intact" - - "Ensure the Medium pizza size radio button was specifically targeted" - -metadata: - tags: ["action", "radio", "click", "form", "httpbin"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-slider-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-slider-001.yaml deleted file mode 100644 index c3706587f0..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-slider-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Range slider test -id: "action-agent-slider-001" -name: "Adjust Range Slider" -description: "Test moving slider to set a specific value" -enabled: true - -target: - url: "https://jqueryui.com/slider/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Move the slider to set the value to 75" - reasoning: "Testing slider/range input manipulation" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the slider control element" - - "Successfully moved the slider handle" - - "Set the slider value to approximately 75" - - "Slider position reflects the target value" - - "Any associated display shows the correct value" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify slider handle moved to represent value 75" - - "Check if value display shows 75 or close to it" - - "Confirm slider position visually matches target" - - "Ensure slider interaction was smooth and successful" - -metadata: - tags: ["action", "slider", "range", "form", "drag"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-tableselect-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-tableselect-001.yaml deleted file mode 100644 index d78e66ca6f..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-tableselect-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Table row selection test -id: "action-agent-tableselect-001" -name: "Select Table Row" -description: "Test clicking to select a table row" -enabled: true - -target: - url: "https://datatables.net/examples/api/select_single_row.html" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click on the first row to select it" - reasoning: "Testing table row selection patterns" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the first table row" - - "Successfully clicked the row" - - "Row became highlighted/selected" - - "Selection state is visually apparent" - - "Only one row is selected at a time" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the first row is now highlighted/selected" - - "Check if row selection visual feedback is clear" - - "Confirm only the clicked row is selected" - - "Ensure row selection styling is properly applied" - -metadata: - tags: ["action", "table", "select", "row", "highlight"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-tablesort-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-tablesort-001.yaml deleted file mode 100644 index e3e3176493..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-tablesort-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Table column sorting test -id: "action-agent-tablesort-001" -name: "Sort Table Column" -description: "Test clicking table column header to sort data" -enabled: true - -target: - url: "https://datatables.net/examples/basic_init/zero_configuration.html" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click on the \"Name\" column header to sort the table by name" - reasoning: "Testing table column sorting interaction" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Name column header" - - "Successfully clicked the column header" - - "Table data reordered by name alphabetically" - - "Sort indicator appeared on the Name column" - - "Table sorting completed without errors" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify table rows are now sorted alphabetically by name" - - "Check if sort arrow/indicator appears on Name column" - - "Confirm the data order changed from before to after" - - "Ensure table structure remained intact after sorting" - -metadata: - tags: ["action", "table", "sort", "column", "data"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-tabs-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-tabs-001.yaml deleted file mode 100644 index 22db60cd57..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-tabs-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Tab panel navigation test -id: "action-agent-tabs-001" -name: "Navigate Tab Panels" -description: "Test clicking tab to switch between tab panels" -enabled: true - -target: - url: "https://jqueryui.com/tabs/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click on the \"Nunc tincidunt\" tab to switch to that panel" - reasoning: "Testing tab panel navigation" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the \"Nunc tincidunt\" tab button" - - "Successfully clicked the tab" - - "Tab panel content switched to the selected tab" - - "Active tab visual state changed appropriately" - - "Content area updated to show the new panel" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the \"Nunc tincidunt\" tab is now active/highlighted" - - "Check if the content panel changed to show new content" - - "Confirm the tab switching animation completed" - - "Ensure the correct tab content is visible" - -metadata: - tags: ["action", "tabs", "navigation", "panels", "ui"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-timepicker-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-timepicker-001.yaml deleted file mode 100644 index 056fbe9c79..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-timepicker-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Time picker test -id: "action-agent-timepicker-001" -name: "Select Time from Picker" -description: "Test setting time using time picker controls" -enabled: true - -target: - url: "https://timepicker.co/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Set the time to 2:30 PM using the time picker controls" - reasoning: "Testing time selection with hour/minute controls" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the time picker interface" - - "Set the hour to 2 (14 for 24-hour format)" - - "Set the minutes to 30" - - "Selected PM or appropriate time format" - - "Time input shows 2:30 PM or equivalent" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the time input displays 2:30 PM or 14:30" - - "Check if hour and minute were set correctly" - - "Confirm AM/PM selection if applicable" - - "Ensure the time picker interface was properly used" - -metadata: - tags: ["action", "timepicker", "time", "form", "clock"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-upload-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-upload-001.yaml deleted file mode 100644 index 518515d61d..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-upload-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# File upload test -id: "action-agent-upload-001" -name: "Upload File via Input" -description: "Test clicking file input and uploading a test file" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/upload" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click the file input and upload a test file" - reasoning: "Testing file upload interaction through input elements" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the file input element" - - "Triggered file selection dialog" - - "Selected a file for upload" - - "File name appears in the input field" - - "Upload process initiated successfully" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify file name appears in the upload input field" - - "Check if file selection was successful" - - "Confirm upload button is available or file is ready" - - "Ensure no upload errors are displayed" - -metadata: - tags: ["action", "upload", "file", "input", "form"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-video-001.yaml b/eval-server/nodejs/evals/action-agent/action-agent-video-001.yaml deleted file mode 100644 index ba21b28e53..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-video-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Video playback controls test -id: "action-agent-video-001" -name: "Control Video Playback" -description: "Test starting video playback using click + spacebar" -enabled: true - -target: - url: "https://www.w3schools.com/html/html5_video.asp" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 90000 - -input: - objective: "Click the video element to focus it, then press spacebar to start playback" - reasoning: "Testing video control using standard keyboard interaction (click to focus + spacebar to play)" - hint: "First click the Video element to focus it, then use keyboard input to press the spacebar key to start playback" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Video element in the accessibility tree" - - "Successfully clicked the Video element to focus it" - - "Used keyboard input to press spacebar" - - "Video playback started after spacebar press" - - "No errors occurred during the interaction sequence" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify video player is visible on the page" - - "Check if the play button was clicked (may show pause button after)" - - "Look for visual indicators that video started playing" - - "Ensure no error messages appeared during video interaction" - -metadata: - tags: ["action", "video", "media", "controls", "playback"] - priority: "high" - timeout: 90000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/action-agent-video-002.yaml b/eval-server/nodejs/evals/action-agent/action-agent-video-002.yaml deleted file mode 100644 index d7188ecd59..0000000000 --- a/eval-server/nodejs/evals/action-agent/action-agent-video-002.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Video play button specific targeting test -id: "action-agent-video-002" -name: "Click Video Play Button Specifically" -description: "Test clicking the specific play button (not the video element)" -enabled: true - -target: - url: "https://www.w3schools.com/html/html5_video.asp" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Find and click the button that has name=\"play\" (not the Video element itself)" - reasoning: "Testing specific targeting of the play button element" - hint: "Target the button element with text or label \"play\", do not click the Video element" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Found a button element (not Video element) with \"play\" in the name" - - "Successfully clicked the play button specifically" - - "Did not click on the Video element itself" - - "Play button click was executed correctly" - - "Video responded to the button click" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the play button (not video element) was clicked" - - "Check if video started playing after button click" - - "Confirm the target was the button, not the video container" - - "Look for changes in video player state" - -metadata: - tags: ["action", "video", "button", "specific-targeting"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/autocomplete-001.yaml b/eval-server/nodejs/evals/action-agent/autocomplete-001.yaml deleted file mode 100644 index 4bd4aa8a33..0000000000 --- a/eval-server/nodejs/evals/action-agent/autocomplete-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Autocomplete search test -id: "autocomplete-001" -name: "Use Autocomplete Search" -description: "Test typing in autocomplete field and selecting from suggestions" -enabled: true - -target: - url: "https://jqueryui.com/autocomplete/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Type \"Java\" in the autocomplete field and select \"JavaScript\" from suggestions" - reasoning: "Testing autocomplete/typeahead interaction patterns" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the autocomplete input field" - - "Typed \"Java\" to trigger suggestions" - - "Autocomplete dropdown appeared with suggestions" - - "Selected \"JavaScript\" from the suggestion list" - - "Input field shows the selected value" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify \"JavaScript\" appears in the input field" - - "Check if autocomplete suggestions appeared" - - "Confirm the correct suggestion was selected" - - "Ensure dropdown closed after selection" - -metadata: - tags: ["action", "autocomplete", "typeahead", "search", "suggestions"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/checkbox-001.yaml b/eval-server/nodejs/evals/action-agent/checkbox-001.yaml deleted file mode 100644 index 041f2f65f1..0000000000 --- a/eval-server/nodejs/evals/action-agent/checkbox-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Checkbox/radio button test -id: "checkbox-001" -name: "Toggle Newsletter Checkbox" -description: "Test clicking checkbox elements for form options" -enabled: true - -target: - url: "https://www.w3schools.com/html/tryit.asp?filename=tryhtml_checkbox" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Click the checkbox labeled \"I have a bike\" to check it" - reasoning: "Testing interaction with checkbox form elements" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Identified the correct checkbox among multiple options" - - "Used click action on the checkbox element" - - "Checkbox state changed from unchecked to checked" - - "Handled the iframe structure if present" - - "No errors with form element interaction" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify the checkbox state changed from unchecked to checked" - - "Confirm the \"I have a bike\" checkbox now shows a checkmark" - - "Verify the checkbox visual indicator (checkmark) is clearly visible" - - "Ensure no other checkboxes were accidentally modified" - -metadata: - tags: ["action", "checkbox", "form", "w3schools", "input"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/checkbox-002.yaml b/eval-server/nodejs/evals/action-agent/checkbox-002.yaml deleted file mode 100644 index 036f388dd1..0000000000 --- a/eval-server/nodejs/evals/action-agent/checkbox-002.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Toggle checkbox test - using HTML form test site -id: "checkbox-002" -name: "Check Extra Cheese Checkbox" -description: "Test checking a specific checkbox using the check method" -enabled: true - -target: - url: "https://httpbin.org/forms/post" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Find and check the \"Extra Cheese\" checkbox in the Pizza Toppings section" - reasoning: "Testing checkbox interaction functionality using check method" - hint: "Look for the Extra Cheese checkbox and use the check method to select it" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Extra Cheese checkbox in the Pizza Toppings section" - - "Used the check method instead of click for better reliability" - - "Checkbox became checked (if it wasn't already)" - - "No errors occurred during checkbox interaction" - - "Form maintained its structure after checkbox selection" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the Extra Cheese checkbox is now checked (shows checkmark)" - - "Check that the checkbox shows proper visual feedback for checked state" - - "Confirm the form structure remained intact" - - "Ensure the checkbox for Extra Cheese was specifically targeted and checked" - -metadata: - tags: ["action", "checkbox", "check", "form", "httpbin"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/click-001.yaml b/eval-server/nodejs/evals/action-agent/click-001.yaml deleted file mode 100644 index e86c8fdd35..0000000000 --- a/eval-server/nodejs/evals/action-agent/click-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Basic search interaction test -id: "click-001" -name: "Search with Text Entry and Click" -description: "Test entering text in search field and clicking search button" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 90000 - -input: - objective: "Type \"DevTools automation\" in the search box and then click the \"Google Search\" button" - reasoning: "Testing multi-step interaction: text input followed by button click" - hint: "First fill the search input field, then find and click the search button" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Successfully located the search input field" - - "Entered \"DevTools automation\" text in the search box" - - "Located the Google Search button after entering text" - - "Successfully clicked the search button" - - "Search was executed and results page loaded" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify text \"DevTools automation\" was entered in the search field" - - "Check if search results page loaded with relevant results" - - "Confirm the search was executed (URL changed to results page)" - - "Ensure search results are related to \"DevTools automation\"" - -metadata: - tags: ["action", "multi-step", "search", "form-fill", "click", "google", "basic"] - priority: "high" - timeout: 90000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/context-001.yaml b/eval-server/nodejs/evals/action-agent/context-001.yaml deleted file mode 100644 index 0ca7c58ccd..0000000000 --- a/eval-server/nodejs/evals/action-agent/context-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Right click context menu test -id: "context-001" -name: "Right Click Context Menu" -description: "Test right-clicking to open context menu" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/context_menu" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Right-click on the context menu area to open the context menu" - reasoning: "Testing right-click context menu interaction" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the designated context menu area" - - "Performed right-click action correctly" - - "Context menu appeared with options" - - "Successfully triggered the right-click event" - - "Alert or confirmation appeared as expected" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify right-click was performed on correct area" - - "Check if context menu or alert appeared" - - "Confirm right-click event was properly triggered" - - "Ensure the expected response occurred" - -metadata: - tags: ["action", "context-menu", "right-click", "mouse", "menu"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/datepicker-001.yaml b/eval-server/nodejs/evals/action-agent/datepicker-001.yaml deleted file mode 100644 index 9b6a9df09b..0000000000 --- a/eval-server/nodejs/evals/action-agent/datepicker-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Date picker test -id: "datepicker-001" -name: "Select Date from Calendar" -description: "Test clicking date input and selecting a specific date from calendar popup" -enabled: true - -target: - url: "https://jqueryui.com/datepicker/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click the date input field and select March 15, 2024 from the calendar picker" - reasoning: "Testing interaction with calendar popup widgets" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located and clicked the date input field" - - "Calendar popup opened successfully" - - "Navigated to correct month/year if needed" - - "Selected the specific date (March 15, 2024)" - - "Date input field shows the selected date" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the date input field contains the selected date" - - "Check if the calendar widget opened and closed properly" - - "Confirm the correct date was highlighted and selected" - - "Ensure the date format matches expected output" - -metadata: - tags: ["action", "datepicker", "calendar", "form", "popup"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/daterange-001.yaml b/eval-server/nodejs/evals/action-agent/daterange-001.yaml deleted file mode 100644 index a9b202b90a..0000000000 --- a/eval-server/nodejs/evals/action-agent/daterange-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Date range picker test -id: "daterange-001" -name: "Select Date Range" -description: "Test selecting a date range with start and end dates" -enabled: true - -target: - url: "https://www.daterangepicker.com/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Select a date range from February 1, 2024 to February 28, 2024" - reasoning: "Testing complex date range selection with start and end dates" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Opened the date range picker interface" - - "Selected the start date (February 1, 2024)" - - "Selected the end date (February 28, 2024)" - - "Date range was properly applied" - - "Input field shows the complete date range" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify both start and end dates are displayed in the input" - - "Check if the date range picker shows the selected range" - - "Confirm the format matches expected date range display" - - "Ensure both dates were selected in sequence" - -metadata: - tags: ["action", "daterange", "date-picker", "form", "complex"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/dropdown-001.yaml b/eval-server/nodejs/evals/action-agent/dropdown-001.yaml deleted file mode 100644 index a64edb0d51..0000000000 --- a/eval-server/nodejs/evals/action-agent/dropdown-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Dropdown selection test -id: "dropdown-001" -name: "Select Dropdown Option" -description: "Test selecting an option from a dropdown menu" -enabled: true - -target: - url: "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_select" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Select \"Audi\" from the car brands dropdown menu" - reasoning: "Testing dropdown selection interaction" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the dropdown/select element" - - "Identified the correct option to select" - - "Successfully selected the Audi option" - - "Dropdown value changed to the selected option" - - "Handled select element interaction properly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify the dropdown selection changed" - - "Confirm \"Audi\" is now displayed as the selected option" - - "Check if the dropdown is closed after selection" - - "Verify no other form elements were affected by the selection" - -metadata: - tags: ["action", "dropdown", "select", "form", "w3schools"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/dynamic-001.yaml b/eval-server/nodejs/evals/action-agent/dynamic-001.yaml deleted file mode 100644 index fba60bd8ab..0000000000 --- a/eval-server/nodejs/evals/action-agent/dynamic-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Dynamic content interaction test -id: "dynamic-001" -name: "Click Dynamic Load Button" -description: "Test clicking a button that loads dynamic content" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/dynamic_loading/1" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 90000 - -input: - objective: "Click the \"Start\" button to trigger dynamic content loading" - reasoning: "Testing interaction with dynamically loaded content" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Found and clicked the Start button" - - "Handled the dynamic loading process" - - "Recognized that content changes after clicking" - - "No timing issues with the dynamic content" - - "Successfully triggered the loading animation" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify dynamic content loaded after clicking Start" - - "Check if loading animation or spinner was displayed" - - "Confirm new content appeared that was previously hidden" - - "Verify the Start button state changed or was replaced after clicking" - -metadata: - tags: ["action", "dynamic", "click", "ajax", "loading"] - priority: "high" - timeout: 90000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/ecommerce-001.yaml b/eval-server/nodejs/evals/action-agent/ecommerce-001.yaml deleted file mode 100644 index ae573de9f6..0000000000 --- a/eval-server/nodejs/evals/action-agent/ecommerce-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# E-commerce action test -id: "ecommerce-001" -name: "Add Product to Cart" -description: "Test clicking \"Add to Cart\" button on an e-commerce product page" -enabled: true - -target: - url: "https://www.homedepot.com/p/Husky-20-Gal-Professional-Duty-Waterproof-Storage-Container-with-Hinged-Lid-in-Red-249160/313799634" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 180000 - -input: - objective: "Click the \"Add to Cart\" button for this storage container" - reasoning: "Testing e-commerce interaction with product cart functionality" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Add to Cart button on the product page" - - "Successfully clicked the button" - - "Handled any popups or confirmations that appeared" - - "Verified the item was added (cart count changed or confirmation shown)" - - "Dealt with page dynamics after clicking" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify the Add to Cart button was clicked" - - "Check if cart count indicator increased or shows the item was added" - - "Look for any confirmation popup or notification about the item being added" - - "Verify the button state changed (e.g., to \"Added to Cart\" or disabled)" - -metadata: - tags: ["action", "ecommerce", "click", "homedepot", "cart"] - priority: "high" - timeout: 180000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/error-001.yaml b/eval-server/nodejs/evals/action-agent/error-001.yaml deleted file mode 100644 index a2b5646a9b..0000000000 --- a/eval-server/nodejs/evals/action-agent/error-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Error recovery test -id: "error-001" -name: "Handle Missing Element" -description: "Test agent behavior when target element is not found" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click the \"Sign Up\" button" - reasoning: "Testing error handling when element does not exist" - hint: "There is no Sign Up button on Google homepage - agent should handle gracefully" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Attempted to find the requested element" - - "Recognized that the element does not exist" - - "Provided clear error message or explanation" - - "Did not crash or produce confusing output" - - "Suggested alternatives or explained the issue" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the page remains in a stable state despite the missing element" - - "Confirm no error dialogs or broken UI elements appeared" - - "Check that the agent handled the missing element gracefully" - - "Ensure the page was properly analyzed even though the target was not found" - -metadata: - tags: ["action", "error-handling", "missing-element", "recovery", "edge-case"] - priority: "high" - timeout: 60000 - retries: 1 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/filter-001.yaml b/eval-server/nodejs/evals/action-agent/filter-001.yaml deleted file mode 100644 index 7efa8f1ab7..0000000000 --- a/eval-server/nodejs/evals/action-agent/filter-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Search filter application test -id: "filter-001" -name: "Apply Search Filters" -description: "Test applying search filters to modify results" -enabled: true - -target: - url: "https://www.w3schools.com/howto/howto_js_filter_lists.asp" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Type \"Anna\" in the search filter to filter the list" - reasoning: "Testing search filter application" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the search filter input" - - "Typed \"Anna\" in the filter field" - - "List items filtered to show only matching results" - - "Non-matching items were hidden or removed from view" - - "Filter functionality worked as expected" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify search input contains \"Anna\"" - - "Check if list shows only items containing \"Anna\"" - - "Confirm non-matching items are not visible" - - "Ensure filter functionality reduced the visible list items" - -metadata: - tags: ["action", "filter", "search", "list", "dynamic"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/form-001.yaml b/eval-server/nodejs/evals/action-agent/form-001.yaml deleted file mode 100644 index c4f06da7e4..0000000000 --- a/eval-server/nodejs/evals/action-agent/form-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Form fill action test -id: "form-001" -name: "Fill Search Query" -description: "Test filling a search input field with specific text" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Fill the search box with \"Chrome DevTools automation testing\"" - reasoning: "Testing form input capability with a specific search query" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Successfully identified the search input field" - - "Used perform_action with fill method" - - "Correctly filled the field with the specified text" - - "Verified the field accepted the input" - - "No formatting or encoding issues with the text" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to confirm text was entered in the search field" - - "Verify the exact text \"Chrome DevTools automation testing\" is visible" - - "Check if search suggestions or autocomplete dropdown appeared" - - "Ensure no input validation errors are shown" - -metadata: - tags: ["action", "form-fill", "input", "google", "basic"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/hover-001.yaml b/eval-server/nodejs/evals/action-agent/hover-001.yaml deleted file mode 100644 index a58b225eaf..0000000000 --- a/eval-server/nodejs/evals/action-agent/hover-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Hover action test -id: "hover-001" -name: "Hover to Reveal Menu" -description: "Test hovering over an element to reveal hidden content" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/hovers" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Hover over the first user avatar image to reveal the hidden caption" - reasoning: "Testing hover interaction to reveal dynamic content" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the first user avatar image" - - "Used appropriate hover action method" - - "Successfully triggered the hover state" - - "Hidden caption became visible after hover" - - "Handled mouse interaction correctly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify hover revealed hidden content" - - "Check that caption or overlay appeared over the first avatar" - - "Confirm the hover state is visually active on the image" - - "Verify user information or caption text is now visible" - -metadata: - tags: ["action", "hover", "mouse", "dynamic", "reveal"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/keyboard-001.yaml b/eval-server/nodejs/evals/action-agent/keyboard-001.yaml deleted file mode 100644 index 6a1ffd1ea0..0000000000 --- a/eval-server/nodejs/evals/action-agent/keyboard-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Keyboard tab navigation test -id: "keyboard-001" -name: "Keyboard Tab Navigation" -description: "Test using keyboard navigation to move between elements" -enabled: true - -target: - url: "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Use Tab key to navigate between menu items and Enter to activate" - reasoning: "Testing keyboard-only navigation patterns" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Successfully used keyboard navigation" - - "Tab key moved focus between menu items" - - "Focus indicators were visible during navigation" - - "Enter key activated the focused menu item" - - "Keyboard navigation followed accessibility standards" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify focus indicators are visible on menu items" - - "Check if keyboard navigation moved focus correctly" - - "Confirm Enter key activated the focused item" - - "Ensure accessibility navigation patterns worked" - -metadata: - tags: ["action", "keyboard", "navigation", "accessibility", "focus"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/login-001.yaml b/eval-server/nodejs/evals/action-agent/login-001.yaml deleted file mode 100644 index b56fbcaf6b..0000000000 --- a/eval-server/nodejs/evals/action-agent/login-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Login form test -id: "login-001" -name: "Fill Login Credentials" -description: "Test filling username and password fields in a login form" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/login" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Fill the username field with \"tomsmith\" and password field with \"SuperSecretPassword!\"" - reasoning: "Testing form fill with multiple fields including password type" - input_data: "tomsmithSuperSecretPassword!" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Identified both username and password fields" - - "Filled username field with correct value" - - "Filled password field with correct value" - - "Handled password field type appropriately" - - "Used the provided input_data XML format correctly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the username field shows \"tomsmith\" entered" - - "Confirm the password field has dots/asterisks indicating password entry" - - "Check that both fields are properly filled before submission" - - "Ensure no validation errors are shown for the filled fields" - -metadata: - tags: ["action", "login", "form-fill", "authentication", "multi-field"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/modal-001.yaml b/eval-server/nodejs/evals/action-agent/modal-001.yaml deleted file mode 100644 index ef05d166f1..0000000000 --- a/eval-server/nodejs/evals/action-agent/modal-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Modal dialog test -id: "modal-001" -name: "Open and Close Modal" -description: "Test opening modal dialog and closing it with X button" -enabled: true - -target: - url: "https://getbootstrap.com/docs/5.0/components/modal/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click to open the modal dialog, then close it using the X button" - reasoning: "Testing modal dialog interaction patterns" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located and clicked the modal trigger button" - - "Modal dialog opened successfully" - - "Modal content was visible and accessible" - - "Found and clicked the close (X) button" - - "Modal closed and page returned to normal state" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify modal opened with visible content" - - "Check if modal overlay appeared correctly" - - "Confirm modal was closed after clicking X" - - "Ensure page background is accessible again" - -metadata: - tags: ["action", "modal", "dialog", "popup", "overlay"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/multiselect-001.yaml b/eval-server/nodejs/evals/action-agent/multiselect-001.yaml deleted file mode 100644 index a456c9b1cf..0000000000 --- a/eval-server/nodejs/evals/action-agent/multiselect-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Multi-select dropdown test -id: "multiselect-001" -name: "Select Multiple Options" -description: "Test selecting multiple options from a multi-select dropdown" -enabled: true - -target: - url: "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_select_multiple" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Select both \"Volvo\" and \"Audi\" from the multi-select dropdown" - reasoning: "Testing multiple selection in select elements" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the multi-select dropdown element" - - "Successfully selected Volvo option" - - "Successfully selected Audi option" - - "Both options remain selected simultaneously" - - "Used appropriate multi-select interaction method" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify both Volvo and Audi appear selected" - - "Check if both options are highlighted/marked" - - "Confirm multi-select functionality worked correctly" - - "Ensure no other options were accidentally selected" - -metadata: - tags: ["action", "multi-select", "dropdown", "form", "multiple"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/multistep-001.yaml b/eval-server/nodejs/evals/action-agent/multistep-001.yaml deleted file mode 100644 index 14923a21a6..0000000000 --- a/eval-server/nodejs/evals/action-agent/multistep-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Multi-step form test -id: "multistep-001" -name: "Complete Search and Submit" -description: "Test filling a search form and then clicking the submit button" -enabled: true - -target: - url: "https://www.bing.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Fill the search box with \"automated testing tools\" and then click the search button" - reasoning: "Testing multi-step form interaction combining fill and click actions" - hint: "This requires two actions: first fill the search field, then click the search button" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Recognized this requires multiple actions" - - "First filled the search input correctly" - - "Then located and clicked the search button" - - "Both actions completed successfully in sequence" - - "Search was initiated with the correct query" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the search input contains \"automated testing tools\" text" - - "Confirm the search was submitted and results page loaded" - - "Check that search results are related to the query" - - "Ensure the multi-step action completed fully with both fill and click" - -metadata: - tags: ["action", "multi-step", "form-fill", "click", "bing", "search"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/nav-001.yaml b/eval-server/nodejs/evals/action-agent/nav-001.yaml deleted file mode 100644 index e1ef6104bc..0000000000 --- a/eval-server/nodejs/evals/action-agent/nav-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Complex navigation test -id: "nav-001" -name: "Navigate via Menu Click" -description: "Test clicking navigation menu items to navigate between pages" -enabled: true - -target: - url: "https://www.wikipedia.org" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click on the \"English\" language link to navigate to English Wikipedia" - reasoning: "Testing navigation through link clicks on a multilingual site" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Identified the correct language link among many options" - - "Successfully clicked the English link" - - "Navigation occurred to the English Wikipedia" - - "Used appropriate tools to verify navigation success" - - "Handled the multilingual page structure correctly" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify navigation from Wikipedia homepage to English Wikipedia" - - "Check if the page language and content changed to English" - - "Verify the URL changed to en.wikipedia.org" - - "Confirm the English Wikipedia main page is displayed" - -metadata: - tags: ["action", "navigation", "click", "wikipedia", "multilingual"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/radio-001.yaml b/eval-server/nodejs/evals/action-agent/radio-001.yaml deleted file mode 100644 index a136e1e8bb..0000000000 --- a/eval-server/nodejs/evals/action-agent/radio-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Radio button selection test -id: "radio-001" -name: "Select Radio Button Option" -description: "Test selecting a specific radio button option using click method" -enabled: true - -target: - url: "https://httpbin.org/forms/post" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 45000 - -input: - objective: "Select the \"Medium\" pizza size from the Pizza Size radio button group" - reasoning: "Testing radio button selection functionality" - hint: "Look for the Medium radio button in the Pizza Size section and click it to select" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Medium radio button in the Pizza Size section" - - "Successfully clicked the Medium radio button" - - "Radio button became selected (checked state)" - - "Other radio buttons in the same group became unselected" - - "Form maintained its structure after radio button selection" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the Medium radio button is now selected (shows filled circle)" - - "Check that other pizza size options (Small, Large) are no longer selected" - - "Confirm the form structure remained intact" - - "Ensure the Medium pizza size radio button was specifically targeted" - -metadata: - tags: ["action", "radio", "click", "form", "httpbin"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/slider-001.yaml b/eval-server/nodejs/evals/action-agent/slider-001.yaml deleted file mode 100644 index 9369671afc..0000000000 --- a/eval-server/nodejs/evals/action-agent/slider-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Range slider test -id: "slider-001" -name: "Adjust Range Slider" -description: "Test moving slider to set a specific value" -enabled: true - -target: - url: "https://jqueryui.com/slider/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Move the slider to set the value to 75" - reasoning: "Testing slider/range input manipulation" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the slider control element" - - "Successfully moved the slider handle" - - "Set the slider value to approximately 75" - - "Slider position reflects the target value" - - "Any associated display shows the correct value" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify slider handle moved to represent value 75" - - "Check if value display shows 75 or close to it" - - "Confirm slider position visually matches target" - - "Ensure slider interaction was smooth and successful" - -metadata: - tags: ["action", "slider", "range", "form", "drag"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/tableselect-001.yaml b/eval-server/nodejs/evals/action-agent/tableselect-001.yaml deleted file mode 100644 index b38341ef20..0000000000 --- a/eval-server/nodejs/evals/action-agent/tableselect-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Table row selection test -id: "tableselect-001" -name: "Select Table Row" -description: "Test clicking to select a table row" -enabled: true - -target: - url: "https://datatables.net/examples/api/select_single_row.html" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click on the first row to select it" - reasoning: "Testing table row selection patterns" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the first table row" - - "Successfully clicked the row" - - "Row became highlighted/selected" - - "Selection state is visually apparent" - - "Only one row is selected at a time" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the first row is now highlighted/selected" - - "Check if row selection visual feedback is clear" - - "Confirm only the clicked row is selected" - - "Ensure row selection styling is properly applied" - -metadata: - tags: ["action", "table", "select", "row", "highlight"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/tablesort-001.yaml b/eval-server/nodejs/evals/action-agent/tablesort-001.yaml deleted file mode 100644 index 32695c729f..0000000000 --- a/eval-server/nodejs/evals/action-agent/tablesort-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Table column sorting test -id: "tablesort-001" -name: "Sort Table Column" -description: "Test clicking table column header to sort data" -enabled: true - -target: - url: "https://datatables.net/examples/basic_init/zero_configuration.html" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click on the \"Name\" column header to sort the table by name" - reasoning: "Testing table column sorting interaction" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Name column header" - - "Successfully clicked the column header" - - "Table data reordered by name alphabetically" - - "Sort indicator appeared on the Name column" - - "Table sorting completed without errors" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify table rows are now sorted alphabetically by name" - - "Check if sort arrow/indicator appears on Name column" - - "Confirm the data order changed from before to after" - - "Ensure table structure remained intact after sorting" - -metadata: - tags: ["action", "table", "sort", "column", "data"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/tabs-001.yaml b/eval-server/nodejs/evals/action-agent/tabs-001.yaml deleted file mode 100644 index 10792663a3..0000000000 --- a/eval-server/nodejs/evals/action-agent/tabs-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Tab panel navigation test -id: "tabs-001" -name: "Navigate Tab Panels" -description: "Test clicking tab to switch between tab panels" -enabled: true - -target: - url: "https://jqueryui.com/tabs/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click on the \"Nunc tincidunt\" tab to switch to that panel" - reasoning: "Testing tab panel navigation" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the \"Nunc tincidunt\" tab button" - - "Successfully clicked the tab" - - "Tab panel content switched to the selected tab" - - "Active tab visual state changed appropriately" - - "Content area updated to show the new panel" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the \"Nunc tincidunt\" tab is now active/highlighted" - - "Check if the content panel changed to show new content" - - "Confirm the tab switching animation completed" - - "Ensure the correct tab content is visible" - -metadata: - tags: ["action", "tabs", "navigation", "panels", "ui"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/timepicker-001.yaml b/eval-server/nodejs/evals/action-agent/timepicker-001.yaml deleted file mode 100644 index cbc5742cd1..0000000000 --- a/eval-server/nodejs/evals/action-agent/timepicker-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Time picker test -id: "timepicker-001" -name: "Select Time from Picker" -description: "Test setting time using time picker controls" -enabled: true - -target: - url: "https://timepicker.co/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Set the time to 2:30 PM using the time picker controls" - reasoning: "Testing time selection with hour/minute controls" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the time picker interface" - - "Set the hour to 2 (14 for 24-hour format)" - - "Set the minutes to 30" - - "Selected PM or appropriate time format" - - "Time input shows 2:30 PM or equivalent" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the time input displays 2:30 PM or 14:30" - - "Check if hour and minute were set correctly" - - "Confirm AM/PM selection if applicable" - - "Ensure the time picker interface was properly used" - -metadata: - tags: ["action", "timepicker", "time", "form", "clock"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/upload-001.yaml b/eval-server/nodejs/evals/action-agent/upload-001.yaml deleted file mode 100644 index d5c276c892..0000000000 --- a/eval-server/nodejs/evals/action-agent/upload-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# File upload test -id: "upload-001" -name: "Upload File via Input" -description: "Test clicking file input and uploading a test file" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/upload" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Click the file input and upload a test file" - reasoning: "Testing file upload interaction through input elements" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the file input element" - - "Triggered file selection dialog" - - "Selected a file for upload" - - "File name appears in the input field" - - "Upload process initiated successfully" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify file name appears in the upload input field" - - "Check if file selection was successful" - - "Confirm upload button is available or file is ready" - - "Ensure no upload errors are displayed" - -metadata: - tags: ["action", "upload", "file", "input", "form"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/video-001.yaml b/eval-server/nodejs/evals/action-agent/video-001.yaml deleted file mode 100644 index 17c76bec8f..0000000000 --- a/eval-server/nodejs/evals/action-agent/video-001.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Video playback controls test -id: "video-001" -name: "Control Video Playback" -description: "Test starting video playback using click + spacebar" -enabled: true - -target: - url: "https://www.w3schools.com/html/html5_video.asp" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 90000 - -input: - objective: "Click the video element to focus it, then press spacebar to start playback" - reasoning: "Testing video control using standard keyboard interaction (click to focus + spacebar to play)" - hint: "First click the Video element to focus it, then use keyboard input to press the spacebar key to start playback" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Located the Video element in the accessibility tree" - - "Successfully clicked the Video element to focus it" - - "Used keyboard input to press spacebar" - - "Video playback started after spacebar press" - - "No errors occurred during the interaction sequence" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify video player is visible on the page" - - "Check if the play button was clicked (may show pause button after)" - - "Look for visual indicators that video started playing" - - "Ensure no error messages appeared during video interaction" - -metadata: - tags: ["action", "video", "media", "controls", "playback"] - priority: "high" - timeout: 90000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/action-agent/video-002.yaml b/eval-server/nodejs/evals/action-agent/video-002.yaml deleted file mode 100644 index b20014caed..0000000000 --- a/eval-server/nodejs/evals/action-agent/video-002.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Video play button specific targeting test -id: "video-002" -name: "Click Video Play Button Specifically" -description: "Test clicking the specific play button (not the video element)" -enabled: true - -target: - url: "https://www.w3schools.com/html/html5_video.asp" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Find and click the button that has name=\"play\" (not the Video element itself)" - reasoning: "Testing specific targeting of the play button element" - hint: "Target the button element with text or label \"play\", do not click the Video element" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Found a button element (not Video element) with \"play\" in the name" - - "Successfully clicked the play button specifically" - - "Did not click on the Video element itself" - - "Play button click was executed correctly" - - "Video responded to the button click" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify the play button (not video element) was clicked" - - "Check if video started playing after button click" - - "Confirm the target was the button, not the video container" - - "Look for changes in video player state" - -metadata: - tags: ["action", "video", "button", "specific-targeting"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/config.yaml b/eval-server/nodejs/evals/config.yaml deleted file mode 100644 index 3968421918..0000000000 --- a/eval-server/nodejs/evals/config.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# model: -# main_model: "deepseek-r1:14b" -# mini_model: "deepseek-r1:14b" -# nano_model: "deepseek-r1:14b" -# provider: "litellm" - -model: - main_model: "gpt-4.1" - mini_model: "gpt-4.1-mini" - nano_model: "gpt-4.1-nano" - provider: "openai" \ No newline at end of file diff --git a/eval-server/nodejs/evals/end-to-end/b-vitamins-research-001.yaml b/eval-server/nodejs/evals/end-to-end/b-vitamins-research-001.yaml deleted file mode 100644 index 746ead6af8..0000000000 --- a/eval-server/nodejs/evals/end-to-end/b-vitamins-research-001.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# B-Vitamins Research - End-to-End Test -id: "vitamins-research-001" -name: "B-Vitamins Supplementation Research" -description: "End-to-end test for comprehensive B-vitamins research using chat interface" -enabled: true - -tool: "chat" -timeout: 600000 - -input: - message: "Research everything on the supplementation of B-vitamins for adults. I need: types of vitamins, available forms and their advantages, dosage and safety" - reasoning: "End-to-end test validating complete user workflow with dynamic tool usage for health research" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Covers all B-vitamin types (B1, B2, B3, B5, B6, B7, B9, B12) comprehensively" - - "Explains different forms of each vitamin and their advantages" - - "Provides appropriate dosage recommendations for adults" - - "Discusses safety considerations and potential side effects" - - "Information is accurate and from reliable health sources" - - "Response is well-organized and easy to understand" - - "Demonstrates intelligent tool selection for health research" - - "Shows complete workflow from request to comprehensive result" - -metadata: - tags: ["end-to-end", "chat", "health", "vitamins", "research", "user-workflow"] - priority: "medium" - timeout: 300000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/end-to-end/investment-research-001.yaml b/eval-server/nodejs/evals/end-to-end/investment-research-001.yaml deleted file mode 100644 index 72014df109..0000000000 --- a/eval-server/nodejs/evals/end-to-end/investment-research-001.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# Renewable Energy Stocks Research - End-to-End Test -id: "investment-research-001" -name: "Renewable Energy Stocks Research" -description: "End-to-end test for investment research using chat interface" -enabled: true - -tool: "chat" -timeout: 600000 - -input: - message: "Research renewable energy stocks for potential investment. Focus on solar and wind companies with market cap over $1B." - reasoning: "End-to-end test validating financial research workflow with dynamic tool usage" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Identifies specific solar and wind energy companies" - - "Confirms companies have market cap over $1 billion" - - "Provides relevant financial metrics and data" - - "Includes business descriptions and growth prospects" - - "Discusses investment considerations and risks" - - "Information appears current and from reliable sources" - - "Demonstrates intelligent financial research tool usage" - - "Shows complete workflow from request to investment analysis" - -metadata: - tags: ["end-to-end", "chat", "investment", "stocks", "renewable-energy", "financial", "user-workflow"] - priority: "medium" - timeout: 300000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/end-to-end/product-comparison-001.yaml b/eval-server/nodejs/evals/end-to-end/product-comparison-001.yaml deleted file mode 100644 index 1363a09398..0000000000 --- a/eval-server/nodejs/evals/end-to-end/product-comparison-001.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Headphones Comparison - End-to-End Test -id: "product-comparison-001" -name: "Noise-Canceling Headphones Comparison" -description: "End-to-end test for product research and comparison using chat interface" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "chat" -timeout: 300000 - -input: - message: "Compare the top 3 noise-canceling headphones under $300. Include features, pros/cons, and where to buy them." - reasoning: "End-to-end test validating product comparison workflow with dynamic tool usage" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Identifies 3 specific noise-canceling headphones under $300" - - "Provides detailed feature comparison for each model" - - "Lists pros and cons for each headphone clearly" - - "Includes pricing information and purchase locations" - - "Comparison is fair and based on objective criteria" - - "Information appears current and accurate" - - "Demonstrates intelligent research and extraction tool usage" - - "Shows complete workflow from request to actionable buying guide" - -metadata: - tags: ["end-to-end", "chat", "product", "comparison", "headphones", "shopping", "user-workflow"] - priority: "medium" - timeout: 300000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/end-to-end/recipe-nutrition-001.yaml b/eval-server/nodejs/evals/end-to-end/recipe-nutrition-001.yaml deleted file mode 100644 index ef8b0f06a5..0000000000 --- a/eval-server/nodejs/evals/end-to-end/recipe-nutrition-001.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Healthy Recipe Search - End-to-End Test -id: "recipe-nutrition-001" -name: "Healthy Family Dinner Recipes" -description: "End-to-end test for recipe search with nutrition criteria using chat interface" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "chat" -timeout: 300000 - -input: - message: "Find me 3 healthy dinner recipes for a family of 4 that are under 500 calories per serving and take less than 30 minutes to prepare." - reasoning: "End-to-end test validating recipe search workflow with specific nutritional and time criteria" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Provides exactly 3 dinner recipes suitable for family of 4" - - "Each recipe is under 500 calories per serving" - - "All recipes can be prepared in under 30 minutes" - - "Includes ingredient lists and cooking instructions" - - "Nutritional information is provided or estimated" - - "Recipes are practical and family-friendly" - - "Demonstrates intelligent recipe search and analysis" - - "Shows complete workflow from request to actionable meal plan" - -metadata: - tags: ["end-to-end", "chat", "recipes", "nutrition", "healthy", "family", "user-workflow"] - priority: "medium" - timeout: 300000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/end-to-end/travel-planning-001.yaml b/eval-server/nodejs/evals/end-to-end/travel-planning-001.yaml deleted file mode 100644 index 401f8b1f2d..0000000000 --- a/eval-server/nodejs/evals/end-to-end/travel-planning-001.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Barcelona Travel Planning - End-to-End Test -id: "travel-planning-001" -name: "Barcelona Trip Planning" -description: "End-to-end test for comprehensive travel planning using chat interface" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "chat" -timeout: 300000 - -input: - message: "Help me plan a 3-day trip to Barcelona. I need flight options from New York, hotel recommendations in the city center, and top 5 attractions to visit." - reasoning: "End-to-end test validating complete travel planning workflow with dynamic tool usage" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Provides realistic flight options from New York to Barcelona" - - "Recommends hotels in Barcelona city center with details" - - "Lists top 5 attractions in Barcelona with descriptions" - - "Information is current and practically useful for trip planning" - - "Includes relevant details like prices, locations, or booking info" - - "Response is well-organized into clear sections" - - "Demonstrates multi-tool usage for comprehensive planning" - - "Shows complete workflow from request to actionable itinerary" - -metadata: - tags: ["end-to-end", "chat", "travel", "planning", "barcelona", "user-workflow"] - priority: "medium" - timeout: 300000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/basic-001.yaml b/eval-server/nodejs/evals/research-agent/basic-001.yaml deleted file mode 100644 index fcd008648a..0000000000 --- a/eval-server/nodejs/evals/research-agent/basic-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Basic research test - stable topic with clear sources -id: "basic-001" -name: "Research Chrome DevTools History" -description: "Research the history and development of Chrome DevTools" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 180000 - -input: - query: "History and development of Chrome DevTools browser developer tools" - reasoning: "Testing basic research capabilities on a well-documented technical topic" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0 - criteria: - - "Research covers the origins and early development of Chrome DevTools" - - "Information includes key milestones and major feature additions" - - "Sources include official documentation or reliable technical sources" - - "At least 3-5 different sources were consulted" - - "Information is factually accurate and up-to-date" - - "Research demonstrates understanding of the topic evolution" - - "Handoff to content_writer_agent occurred with comprehensive data" - -metadata: - tags: ["basic", "technical", "stable", "documentation"] - priority: "high" - timeout: 180000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/business-001.yaml b/eval-server/nodejs/evals/research-agent/business-001.yaml deleted file mode 100644 index 7558120d4b..0000000000 --- a/eval-server/nodejs/evals/research-agent/business-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Business research test -id: "business-001" -name: "Research Remote Work Productivity" -description: "Research remote work impact on productivity and business outcomes" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 240000 - -input: - query: "Remote work productivity statistics impact business outcomes 2024 studies" - reasoning: "Testing business research requiring statistical data and multiple perspectives" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Research includes statistical data and survey results" - - "Covers multiple perspectives (employee, employer, industry)" - - "Sources include business publications, research studies, and reports" - - "Information addresses both positive and negative impacts" - - "Data is recent and relevant to current work trends" - - "Research demonstrates understanding of business implications" - - "Statistics and claims are properly sourced" - -metadata: - tags: ["business", "statistics", "workplace", "comprehensive"] - priority: "high" - timeout: 240000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/comparison-001.yaml b/eval-server/nodejs/evals/research-agent/comparison-001.yaml deleted file mode 100644 index a9aa22bfb0..0000000000 --- a/eval-server/nodejs/evals/research-agent/comparison-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Comparative research test -id: "comparison-001" -name: "Compare JavaScript vs TypeScript" -description: "Research and compare JavaScript and TypeScript for web development" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 200000 - -input: - query: "JavaScript vs TypeScript comparison web development pros cons differences" - reasoning: "Testing comparative research requiring balanced analysis of multiple options" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Research covers both JavaScript and TypeScript comprehensively" - - "Includes clear comparison points (syntax, features, ecosystem)" - - "Presents advantages and disadvantages of each language" - - "Sources include technical documentation and developer resources" - - "Information is balanced and objective, not biased toward one option" - - "Demonstrates understanding of use cases for each language" - - "Research data is well-organized for comparative analysis" - -metadata: - tags: ["comparison", "technical", "programming", "balanced"] - priority: "high" - timeout: 200000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/current-001.yaml b/eval-server/nodejs/evals/research-agent/current-001.yaml deleted file mode 100644 index 6878868a67..0000000000 --- a/eval-server/nodejs/evals/research-agent/current-001.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Current events research test -id: "current-001" -name: "Research Latest AI Development Trends" -description: "Research recent developments in AI and machine learning (last 6 months)" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 240000 - -input: - query: "Latest AI artificial intelligence developments breakthroughs 2024 2025" - reasoning: "Testing research on current events and rapidly evolving topics" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - include_url: true - criteria: - - "Research focuses on recent developments (within last 6 months)" - - "Covers multiple aspects of AI development (models, applications, research)" - - "Sources are current and from reputable news or research outlets" - - "Information includes specific examples or case studies" - - "Demonstrates ability to identify current trends vs older information" - - "Successfully gathered information from diverse source types" - - "Data is properly organized for content writer handoff" - -metadata: - tags: ["current-events", "ai", "dynamic", "trends"] - priority: "high" - timeout: 240000 - retries: 1 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/edge-001.yaml b/eval-server/nodejs/evals/research-agent/edge-001.yaml deleted file mode 100644 index d75c2bf0d2..0000000000 --- a/eval-server/nodejs/evals/research-agent/edge-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# No-results edge case test -id: "edge-001" -name: "Research Obscure Fictional Topic" -description: "Test handling of queries with very limited or no reliable sources" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 180000 - -input: - query: "quantum bluetooth watermelon encryption algorithm 2024" - reasoning: "Testing edge case handling when query yields no meaningful results" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Agent recognizes when query yields limited or unreliable results" - - "Demonstrates appropriate search strategy modification" - - "Does not fabricate information when sources are unavailable" - - "Gracefully handles lack of substantive results" - - "Still attempts handoff to content writer with available information" - - "Maintains professional approach despite limited data" - - "Shows appropriate uncertainty when information is sparse" - -metadata: - tags: ["edge-case", "no-results", "error-handling", "fictional"] - priority: "high" - timeout: 180000 - retries: 1 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/research-agent-basic-001.yaml b/eval-server/nodejs/evals/research-agent/research-agent-basic-001.yaml deleted file mode 100644 index 85743d55c3..0000000000 --- a/eval-server/nodejs/evals/research-agent/research-agent-basic-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Basic research test - stable topic with clear sources -id: "research-agent-basic-001" -name: "Research Chrome DevTools History" -description: "Research the history and development of Chrome DevTools" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 180000 - -input: - query: "History and development of Chrome DevTools browser developer tools" - reasoning: "Testing basic research capabilities on a well-documented technical topic" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0 - criteria: - - "Research covers the origins and early development of Chrome DevTools" - - "Information includes key milestones and major feature additions" - - "Sources include official documentation or reliable technical sources" - - "At least 3-5 different sources were consulted" - - "Information is factually accurate and up-to-date" - - "Research demonstrates understanding of the topic evolution" - - "Handoff to content_writer_agent occurred with comprehensive data" - -metadata: - tags: ["basic", "technical", "stable", "documentation"] - priority: "high" - timeout: 180000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/research-agent-business-001.yaml b/eval-server/nodejs/evals/research-agent/research-agent-business-001.yaml deleted file mode 100644 index defeed16e6..0000000000 --- a/eval-server/nodejs/evals/research-agent/research-agent-business-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Business research test -id: "research-agent-business-001" -name: "Research Remote Work Productivity" -description: "Research remote work impact on productivity and business outcomes" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 240000 - -input: - query: "Remote work productivity statistics impact business outcomes 2024 studies" - reasoning: "Testing business research requiring statistical data and multiple perspectives" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Research includes statistical data and survey results" - - "Covers multiple perspectives (employee, employer, industry)" - - "Sources include business publications, research studies, and reports" - - "Information addresses both positive and negative impacts" - - "Data is recent and relevant to current work trends" - - "Research demonstrates understanding of business implications" - - "Statistics and claims are properly sourced" - -metadata: - tags: ["business", "statistics", "workplace", "comprehensive"] - priority: "high" - timeout: 240000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/research-agent-comparison-001.yaml b/eval-server/nodejs/evals/research-agent/research-agent-comparison-001.yaml deleted file mode 100644 index a433a58d88..0000000000 --- a/eval-server/nodejs/evals/research-agent/research-agent-comparison-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Comparative research test -id: "research-agent-comparison-001" -name: "Compare JavaScript vs TypeScript" -description: "Research and compare JavaScript and TypeScript for web development" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 200000 - -input: - query: "JavaScript vs TypeScript comparison web development pros cons differences" - reasoning: "Testing comparative research requiring balanced analysis of multiple options" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Research covers both JavaScript and TypeScript comprehensively" - - "Includes clear comparison points (syntax, features, ecosystem)" - - "Presents advantages and disadvantages of each language" - - "Sources include technical documentation and developer resources" - - "Information is balanced and objective, not biased toward one option" - - "Demonstrates understanding of use cases for each language" - - "Research data is well-organized for comparative analysis" - -metadata: - tags: ["comparison", "technical", "programming", "balanced"] - priority: "high" - timeout: 200000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/research-agent-current-001.yaml b/eval-server/nodejs/evals/research-agent/research-agent-current-001.yaml deleted file mode 100644 index 198c981c82..0000000000 --- a/eval-server/nodejs/evals/research-agent/research-agent-current-001.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Current events research test -id: "research-agent-current-001" -name: "Research Latest AI Development Trends" -description: "Research recent developments in AI and machine learning (last 6 months)" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 240000 - -input: - query: "Latest AI artificial intelligence developments breakthroughs 2024 2025" - reasoning: "Testing research on current events and rapidly evolving topics" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - include_url: true - criteria: - - "Research focuses on recent developments (within last 6 months)" - - "Covers multiple aspects of AI development (models, applications, research)" - - "Sources are current and from reputable news or research outlets" - - "Information includes specific examples or case studies" - - "Demonstrates ability to identify current trends vs older information" - - "Successfully gathered information from diverse source types" - - "Data is properly organized for content writer handoff" - -metadata: - tags: ["current-events", "ai", "dynamic", "trends"] - priority: "high" - timeout: 240000 - retries: 1 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/research-agent-edge-001.yaml b/eval-server/nodejs/evals/research-agent/research-agent-edge-001.yaml deleted file mode 100644 index 234c832fe9..0000000000 --- a/eval-server/nodejs/evals/research-agent/research-agent-edge-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# No-results edge case test -id: "research-agent-edge-001" -name: "Research Obscure Fictional Topic" -description: "Test handling of queries with very limited or no reliable sources" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 180000 - -input: - query: "quantum bluetooth watermelon encryption algorithm 2024" - reasoning: "Testing edge case handling when query yields no meaningful results" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Agent recognizes when query yields limited or unreliable results" - - "Demonstrates appropriate search strategy modification" - - "Does not fabricate information when sources are unavailable" - - "Gracefully handles lack of substantive results" - - "Still attempts handoff to content writer with available information" - - "Maintains professional approach despite limited data" - - "Shows appropriate uncertainty when information is sparse" - -metadata: - tags: ["edge-case", "no-results", "error-handling", "fictional"] - priority: "high" - timeout: 180000 - retries: 1 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/research-agent-technical-001.yaml b/eval-server/nodejs/evals/research-agent/research-agent-technical-001.yaml deleted file mode 100644 index c5e25408d5..0000000000 --- a/eval-server/nodejs/evals/research-agent/research-agent-technical-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Deep technical research test -id: "research-agent-technical-001" -name: "Research WebAssembly Performance" -description: "Deep dive research into WebAssembly performance characteristics and use cases" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 900000 - -input: - query: "WebAssembly WASM performance benchmarks use cases implementation details" - reasoning: "Testing deep technical research requiring specialized knowledge synthesis" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Research covers technical details of WebAssembly architecture" - - "Includes performance benchmarks and comparison data" - - "Discusses practical use cases and implementation scenarios" - - "Sources include technical specifications, benchmarks, and expert analysis" - - "Information demonstrates deep understanding of the technology" - - "Research addresses both benefits and limitations" - - "Technical accuracy is maintained throughout" - -metadata: - tags: ["technical", "deep-dive", "performance", "webassembly"] - priority: "high" - timeout: 900000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/research-agent-tools-001.yaml b/eval-server/nodejs/evals/research-agent/research-agent-tools-001.yaml deleted file mode 100644 index 44da108d76..0000000000 --- a/eval-server/nodejs/evals/research-agent/research-agent-tools-001.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Tool orchestration test - focuses on how well the agent uses available tools -id: "research-agent-tools-001" -name: "Research Python Framework Comparison" -description: "Research comparing Django vs Flask Python frameworks with focus on tool usage" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 240000 - -input: - query: "Django vs Flask Python web framework comparison features performance" - reasoning: "Testing effective orchestration of navigation, extraction, and fetching tools" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Agent effectively used navigate_url to access search engines" - - "Schema-based extraction was used to gather structured search results" - - "Fetcher tool was used to collect content from multiple URLs" - - "Navigation strategy was logical and systematic" - - "Tool usage demonstrated purposeful research progression" - - "Information from different tools was effectively synthesized" - - "At least 3-5 different sources were accessed and processed" - - "Final handoff included comprehensive data from all tools" - -metadata: - tags: ["tool-orchestration", "systematic", "python", "frameworks"] - priority: "high" - timeout: 240000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/technical-001.yaml b/eval-server/nodejs/evals/research-agent/technical-001.yaml deleted file mode 100644 index f434081c88..0000000000 --- a/eval-server/nodejs/evals/research-agent/technical-001.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Deep technical research test -id: "technical-001" -name: "Research WebAssembly Performance" -description: "Deep dive research into WebAssembly performance characteristics and use cases" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 900000 - -input: - query: "WebAssembly WASM performance benchmarks use cases implementation details" - reasoning: "Testing deep technical research requiring specialized knowledge synthesis" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Research covers technical details of WebAssembly architecture" - - "Includes performance benchmarks and comparison data" - - "Discusses practical use cases and implementation scenarios" - - "Sources include technical specifications, benchmarks, and expert analysis" - - "Information demonstrates deep understanding of the technology" - - "Research addresses both benefits and limitations" - - "Technical accuracy is maintained throughout" - -metadata: - tags: ["technical", "deep-dive", "performance", "webassembly"] - priority: "high" - timeout: 900000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/research-agent/tools-001.yaml b/eval-server/nodejs/evals/research-agent/tools-001.yaml deleted file mode 100644 index ae974307e7..0000000000 --- a/eval-server/nodejs/evals/research-agent/tools-001.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Tool orchestration test - focuses on how well the agent uses available tools -id: "tools-001" -name: "Research Python Framework Comparison" -description: "Research comparing Django vs Flask Python frameworks with focus on tool usage" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "research_agent" -timeout: 240000 - -input: - query: "Django vs Flask Python web framework comparison features performance" - reasoning: "Testing effective orchestration of navigation, extraction, and fetching tools" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Agent effectively used navigate_url to access search engines" - - "Schema-based extraction was used to gather structured search results" - - "Fetcher tool was used to collect content from multiple URLs" - - "Navigation strategy was logical and systematic" - - "Tool usage demonstrated purposeful research progression" - - "Information from different tools was effectively synthesized" - - "At least 3-5 different sources were accessed and processed" - - "Final handoff included comprehensive data from all tools" - -metadata: - tags: ["tool-orchestration", "systematic", "python", "frameworks"] - priority: "high" - timeout: 240000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/amazon-product-001.yaml b/eval-server/nodejs/evals/schema-extractor/amazon-product-001.yaml deleted file mode 100644 index 42e4738f0a..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/amazon-product-001.yaml +++ /dev/null @@ -1,78 +0,0 @@ -# E-commerce product extraction test -id: "amazon-product-001" -name: "Extract Amazon Product Details" -description: "Extract product information from an Amazon product page" -enabled: true - -target: - url: "https://www.amazon.com/Obelisk-Climbing-Rustproof-Trellises-Clematis/dp/B0B4SBY6QD/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 60000 - -input: - schema: - type: "object" - properties: - product: - type: "object" - properties: - title: - type: "string" - brand: - type: "string" - price: - type: "object" - properties: - current: - type: "number" - currency: - type: "string" - rating: - type: "object" - properties: - average: - type: "number" - count: - type: "number" - images: - type: "array" - items: - type: "string" - format: "url" - features: - type: "array" - items: - type: "string" - required: - - "title" - - "price" - availability: - type: "string" - required: - - "product" - instruction: "Extract comprehensive product information including pricing, ratings, and key features" - reasoning: "Testing extraction from a dynamic e-commerce page with complex structure" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Product title is accurate and complete" - - "Price information is current and properly formatted" - - "Rating data includes both average and review count" - - "Image URLs are valid and accessible" - - "Key product features are captured" - - "All URLs are properly resolved (not node IDs)" - -metadata: - tags: ["ecommerce", "amazon", "product", "dynamic"] - priority: "high" - timeout: 60000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/bbc-news-001.yaml b/eval-server/nodejs/evals/schema-extractor/bbc-news-001.yaml deleted file mode 100644 index 6843147159..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/bbc-news-001.yaml +++ /dev/null @@ -1,69 +0,0 @@ -# News article extraction test -id: "bbc-news-001" -name: "Extract BBC News Article" -description: "Extract article content and metadata from a BBC News page" -enabled: true - -target: - url: "https://www.bbc.com/news/technology" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 30000 - -input: - schema: - type: "object" - properties: - headlines: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - summary: - type: "string" - url: - type: "string" - format: "url" - category: - type: "string" - required: - - "title" - mainStory: - type: "object" - properties: - headline: - type: "string" - summary: - type: "string" - url: - type: "string" - format: "url" - required: - - "headlines" - instruction: "Extract the main headlines and featured stories from the BBC Technology news section" - reasoning: "Testing extraction from a news aggregation page with multiple articles" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - include_url: true - criteria: - - "Headlines are current and relevant to technology news" - - "Article summaries provide meaningful context" - - "URLs link to valid BBC news articles" - - "Main story is properly identified" - - "All extracted content is in English" - -metadata: - tags: ["news", "bbc", "aggregation", "dynamic"] - priority: "high" - timeout: 30000 - retries: 2 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/bing-search-001.yaml b/eval-server/nodejs/evals/schema-extractor/bing-search-001.yaml deleted file mode 100644 index 7e7d674c2d..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/bing-search-001.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# Bing Search results extraction test -id: "bing-search-001" -name: "Extract Bing Search Results" -description: "Extract search results from Bing search page" -enabled: true - -target: - url: "https://www.bing.com/search?q=web+scraping+best+practices" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 45000 - -input: - schema: - type: "object" - properties: - query: - type: "string" - searchResults: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - url: - type: "string" - format: "url" - snippet: - type: "string" - datePublished: - type: "string" - required: - - "title" - - "url" - - "snippet" - sidebarInfo: - type: "object" - properties: - title: - type: "string" - description: - type: "string" - source: - type: "string" - required: - - "searchResults" - instruction: "Extract search results including titles, URLs, snippets, and any sidebar information from Bing" - reasoning: "Testing extraction from Bing search results with different layout than Google" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Search results match the query intent" - - "Results include valid URLs and meaningful snippets" - - "Sidebar information is extracted when present" - - "No duplicate results in the list" - -metadata: - tags: ["search", "bing", "serp", "dynamic"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/github-repo-001-streamlined.yaml b/eval-server/nodejs/evals/schema-extractor/github-repo-001-streamlined.yaml deleted file mode 100644 index 07532e7d13..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/github-repo-001-streamlined.yaml +++ /dev/null @@ -1,66 +0,0 @@ -# Simple structured data test (Streamlined version) -id: "github-repo-001-streamlined" -name: "Extract GitHub Repository Info (Streamlined)" -description: "Extract basic repository information from a GitHub page using streamlined extractor" -enabled: true - -target: - url: "https://github.com/microsoft/TypeScript" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 30000 - -input: - schema: - type: "object" - properties: - name: - type: "string" - description: - type: "string" - language: - type: "string" - stars: - type: "number" - forks: - type: "number" - topics: - type: "array" - items: - type: "string" - readme: - type: "object" - properties: - summary: - type: "string" - required: - - "name" - - "description" - instruction: "Extract repository metadata and basic statistics" - reasoning: "Testing extraction from a well-structured GitHub repository page" - -validation: - type: "hybrid" - snapshot: - exclude_paths: - - "stars" - - "forks" - structure_only: false - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Repository name matches the GitHub page" - - "Description accurately reflects the project purpose" - - "Programming language is correctly identified" - - "Topic tags are relevant to the project" - -metadata: - tags: ["github", "repository", "structured", "streamlined"] - priority: "high" - timeout: 30000 - retries: 1 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/github-repo-001.yaml b/eval-server/nodejs/evals/schema-extractor/github-repo-001.yaml deleted file mode 100644 index 669357779b..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/github-repo-001.yaml +++ /dev/null @@ -1,66 +0,0 @@ -# Simple structured data test -id: "github-repo-001" -name: "Extract GitHub Repository Info" -description: "Extract basic repository information from a GitHub page" -enabled: true - -target: - url: "https://github.com/microsoft/TypeScript" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 30000 - -input: - schema: - type: "object" - properties: - name: - type: "string" - description: - type: "string" - language: - type: "string" - stars: - type: "number" - forks: - type: "number" - topics: - type: "array" - items: - type: "string" - readme: - type: "object" - properties: - summary: - type: "string" - required: - - "name" - - "description" - instruction: "Extract repository metadata and basic statistics" - reasoning: "Testing extraction from a well-structured GitHub repository page" - -validation: - type: "hybrid" - snapshot: - exclude_paths: - - "stars" - - "forks" - structure_only: false - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Repository name matches the GitHub page" - - "Description accurately reflects the project purpose" - - "Programming language is correctly identified" - - "Topic tags are relevant to the project" - -metadata: - tags: ["github", "repository", "structured"] - priority: "high" - timeout: 30000 - retries: 1 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/google-flights-001.yaml b/eval-server/nodejs/evals/schema-extractor/google-flights-001.yaml deleted file mode 100644 index ab2e53c91e..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/google-flights-001.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# Google Flights search extraction test -id: "google-flights-001" -name: "Extract Google Flights Search Results" -description: "Extract flight options from Google Flights search" -enabled: true - -target: - url: "https://www.google.com/travel/flights/search?tfs=CBwQAhojEgoyMDI1LTEyLTI0agwIAhIIL20vMGQ5anJyBwgBEgNTRk8aIxIKMjAyNS0xMi0zMWoHCAESA1NGT3IMCAISCC9tLzBkOWpyQAFIAXABggELCP___________wGYAQE" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 60000 - -input: - schema: - type: "object" - properties: - searchCriteria: - type: "object" - properties: - origin: - type: "string" - destination: - type: "string" - departureDate: - type: "string" - returnDate: - type: "string" - tripType: - type: "string" - passengers: - type: "number" - flights: - type: "array" - items: - type: "object" - properties: - airline: - type: "string" - flightNumber: - type: "string" - departureTime: - type: "string" - arrivalTime: - type: "string" - duration: - type: "string" - stops: - type: "number" - price: - type: "object" - properties: - amount: - type: "number" - currency: - type: "string" - cabin: - type: "string" - bookingUrl: - type: "string" - format: "url" - legroom: - type: "string" - amenities: - type: "array" - items: - type: "string" - required: - - "airline" - - "departureTime" - - "arrivalTime" - - "price" - priceInsights: - type: "object" - properties: - trend: - type: "string" - recommendation: - type: "string" - averagePrice: - type: "number" - required: - - "flights" - instruction: "Extract flight options including airlines, times, prices, and amenities from Google Flights results" - reasoning: "Testing extraction from complex travel search interface with dynamic pricing" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Flight times are in proper format" - - "Prices are numeric values with currency" - - "Airlines and flight numbers are accurate" - - "Stop information is correctly identified" - - "Duration is in readable format" - -metadata: - tags: ["travel", "flights", "google", "booking"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/google-search-001.yaml b/eval-server/nodejs/evals/schema-extractor/google-search-001.yaml deleted file mode 100644 index 5763ba83a3..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/google-search-001.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# Google Search results extraction test -id: "google-search-001" -name: "Extract Google Search Results" -description: "Extract search results from Google search page" -enabled: true - -target: - url: "https://www.google.com/search?q=chrome+devtools+tutorial" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 45000 - -input: - schema: - type: "object" - properties: - query: - type: "string" - searchResults: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - url: - type: "string" - format: "url" - snippet: - type: "string" - domain: - type: "string" - required: - - "title" - - "url" - - "snippet" - featuredSnippet: - type: "object" - properties: - content: - type: "string" - source: - type: "string" - url: - type: "string" - format: "url" - relatedSearches: - type: "array" - items: - type: "string" - required: - - "searchResults" - instruction: "Extract the top 10 search results with titles, URLs, and snippets. Also extract featured snippet if present and related searches" - reasoning: "Testing extraction from Google search results page with various result types" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Search results are relevant to the query" - - "Each result has a valid title, URL, and snippet" - - "URLs are properly resolved and not node IDs" - - "Related searches are extracted if present" - - "Featured snippet is captured when available" - -metadata: - tags: ["search", "google", "serp", "dynamic"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/homedepot-001.yaml b/eval-server/nodejs/evals/schema-extractor/homedepot-001.yaml deleted file mode 100644 index 2eb4883ffd..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/homedepot-001.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# Home Depot product search extraction test -id: "homedepot-001" -name: "Extract Home Depot Product Search" -description: "Extract product listings from Home Depot search results" -enabled: true - -target: - url: "https://www.homedepot.com/s/power%2520drill" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 60000 - -input: - schema: - type: "object" - properties: - searchQuery: - type: "string" - totalResults: - type: "number" - products: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - brand: - type: "string" - price: - type: "number" - originalPrice: - type: "number" - savings: - type: "number" - rating: - type: "number" - reviewCount: - type: "number" - productUrl: - type: "string" - format: "url" - imageUrl: - type: "string" - format: "url" - availability: - type: "string" - features: - type: "array" - items: - type: "string" - required: - - "name" - - "price" - - "productUrl" - filters: - type: "object" - properties: - brands: - type: "array" - items: - type: "string" - priceRanges: - type: "array" - items: - type: "string" - required: - - "products" - instruction: "Extract product listings from Home Depot search results including prices, ratings, and availability" - reasoning: "Testing extraction from e-commerce search results with product cards and filters" - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Products are relevant to the search query" - - "Prices are numeric values in USD" - - "Product URLs link to Home Depot product pages" - - "Ratings are on a 5-star scale" - - "Key product features are captured" - -metadata: - tags: ["ecommerce", "homedepot", "products", "search"] - priority: "high" - timeout: 60000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/macys-001.yaml b/eval-server/nodejs/evals/schema-extractor/macys-001.yaml deleted file mode 100644 index 81e05f9690..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/macys-001.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# Macy's product listing extraction test -id: "macys-001" -name: "Extract Macy's Product Listings" -description: "Extract fashion products from Macy's category page" -enabled: true - -target: - url: "https://www.macys.com/shop/womens-clothing/womens-dresses" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 60000 - -input: - schema: - type: "object" - properties: - category: - type: "string" - totalProducts: - type: "number" - products: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - brand: - type: "string" - currentPrice: - type: "number" - originalPrice: - type: "number" - discount: - type: "string" - colors: - type: "array" - items: - type: "string" - sizes: - type: "array" - items: - type: "string" - rating: - type: "number" - reviewCount: - type: "number" - productUrl: - type: "string" - format: "url" - imageUrl: - type: "string" - format: "url" - promotions: - type: "array" - items: - type: "string" - required: - - "name" - - "brand" - - "currentPrice" - refinements: - type: "object" - properties: - brands: - type: "array" - items: - type: "string" - sizes: - type: "array" - items: - type: "string" - colors: - type: "array" - items: - type: "string" - priceRanges: - type: "array" - items: - type: "string" - required: - - "products" - instruction: "Extract fashion products including prices, sizes, colors, and promotional offers from Macy's" - reasoning: "Testing extraction from fashion e-commerce with complex product attributes" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Products are from the correct category" - - "Prices reflect current and sale prices" - - "Color and size options are captured" - - "Brand names are accurately extracted" - - "Promotional text is included when present" - -metadata: - tags: ["ecommerce", "macys", "fashion", "products"] - priority: "high" - timeout: 60000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/schema-extractor/wikipedia-search-001.yaml b/eval-server/nodejs/evals/schema-extractor/wikipedia-search-001.yaml deleted file mode 100644 index 616f0d6a82..0000000000 --- a/eval-server/nodejs/evals/schema-extractor/wikipedia-search-001.yaml +++ /dev/null @@ -1,77 +0,0 @@ -# Wikipedia search results extraction test -id: "wikipedia-search-001" -name: "Extract Wikipedia Search Results" -description: "Extract search results from Wikipedia search" -enabled: true - -target: - url: "https://en.wikipedia.org/w/index.php?search=artificial+intelligence&title=Special:Search" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_data" -timeout: 30000 - -input: - schema: - type: "object" - properties: - searchTerm: - type: "string" - resultCount: - type: "number" - searchResults: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - url: - type: "string" - format: "url" - snippet: - type: "string" - category: - type: "string" - wordCount: - type: "number" - lastEdited: - type: "string" - required: - - "title" - - "url" - - "snippet" - suggestedArticles: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - url: - type: "string" - format: "url" - required: - - "searchResults" - instruction: "Extract Wikipedia search results including article titles, URLs, snippets, and metadata like word count or last edit date" - reasoning: "Testing extraction from Wikipedia's internal search with rich metadata" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Search results are Wikipedia articles" - - "Each result has a valid Wikipedia URL" - - "Snippets contain relevant content highlights" - - "Metadata like word count is extracted when available" - -metadata: - tags: ["search", "wikipedia", "encyclopedia"] - priority: "high" - timeout: 30000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/screenshot-verification/dynamic-content-verification-001.yaml b/eval-server/nodejs/evals/screenshot-verification/dynamic-content-verification-001.yaml deleted file mode 100644 index 6ec53c4a35..0000000000 --- a/eval-server/nodejs/evals/screenshot-verification/dynamic-content-verification-001.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Dynamic content visual verification test -id: "dynamic-content-verification-001" -name: "Dynamic Content Visual Verification" -description: "Test visual verification of dynamic content loading using screenshots" -enabled: true - -target: - url: "https://the-internet.herokuapp.com/dynamic_loading/1" - -tool: "action_agent" -timeout: 90000 - -input: - objective: "Take a screenshot, click the Start button, wait for content to load, then take another screenshot to verify the dynamic content appeared" - reasoning: "Testing visual verification of dynamic content changes using screenshot comparison" - hint: "Use take_screenshot before clicking Start, then again after the dynamic content loads" - - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o" - criteria: - - "Initial screenshot captured the page before dynamic loading" - - "Start button was successfully clicked" - - "Agent waited for dynamic content to fully load" - - "Final screenshot shows the revealed dynamic content" - - "Visual comparison demonstrates successful content loading verification" - - "Screenshots show clear before/after difference in content visibility" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare screenshots to verify dynamic content loading" - - "Confirm the first screenshot shows hidden content area" - - "Verify the second screenshot shows the revealed 'Hello World!' text" - - "Check that the loading animation or process is properly captured" - -metadata: - tags: ["screenshot", "dynamic-content", "visual-verification", "loading"] - priority: "high" - timeout: 90000 - retries: 2 - flaky: true \ No newline at end of file diff --git a/eval-server/nodejs/evals/screenshot-verification/screenshot-error-handling-001.yaml b/eval-server/nodejs/evals/screenshot-verification/screenshot-error-handling-001.yaml deleted file mode 100644 index 6d31c5080c..0000000000 --- a/eval-server/nodejs/evals/screenshot-verification/screenshot-error-handling-001.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Screenshot error handling test -id: "screenshot-error-handling-001" -name: "Screenshot Error Handling" -description: "Test screenshot tool error handling and recovery" -enabled: true - -target: - url: "https://httpstat.us/500" - -tool: "take_screenshot" -timeout: 30000 - -input: - fullPage: false - - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o" - criteria: - - "Screenshot tool handled the error page gracefully" - - "Either successfully captured the error page or reported appropriate error" - - "No crashes or undefined behavior occurred" - - "Tool response is meaningful regardless of page loading issues" - - "Error handling demonstrates robustness of screenshot functionality" - visual_verification: - enabled: true - capture_before: false - capture_after: true - prompts: - - "If screenshot was taken, verify it shows the error page content" - - "Check that the tool handled the HTTP 500 error appropriately" - - "Confirm no blank or corrupted screenshots were produced" - - "Ensure error scenarios are handled professionally" - -metadata: - tags: ["screenshot", "error-handling", "robustness", "edge-case"] - priority: "normal" - timeout: 30000 - retries: 1 - flaky: true \ No newline at end of file diff --git a/eval-server/nodejs/evals/screenshot-verification/screenshot-fullpage-001.yaml b/eval-server/nodejs/evals/screenshot-verification/screenshot-fullpage-001.yaml deleted file mode 100644 index a1c71f9099..0000000000 --- a/eval-server/nodejs/evals/screenshot-verification/screenshot-fullpage-001.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# Full page screenshot verification test -id: "screenshot-fullpage-001" -name: "Take Full Page Screenshot" -description: "Test taking full page screenshot and verify functionality" -enabled: true - -target: - url: "https://en.wikipedia.org/wiki/Chrome_DevTools" - -tool: "take_screenshot" -timeout: 45000 - -input: - fullPage: true - - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o" - criteria: - - "Full page screenshot was successfully captured" - - "Data URL contains valid image data" - - "Screenshot captures the entire page content including areas below the fold" - - "Image size is larger than viewport-only screenshot would be" - - "No errors occurred during full page capture" - - "Screenshot includes both header and footer content" - visual_verification: - enabled: true - capture_before: false - capture_after: true - prompts: - - "Verify the screenshot shows the complete Wikipedia article page" - - "Check that content above and below the fold is captured" - - "Confirm the image is taller than a typical viewport" - - "Ensure no content is cut off at the bottom" - -metadata: - tags: ["screenshot", "fullpage", "visual", "verification", "wikipedia"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: false \ No newline at end of file diff --git a/eval-server/nodejs/evals/screenshot-verification/screenshot-viewport-001.yaml b/eval-server/nodejs/evals/screenshot-verification/screenshot-viewport-001.yaml deleted file mode 100644 index 69531eefbe..0000000000 --- a/eval-server/nodejs/evals/screenshot-verification/screenshot-viewport-001.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Viewport screenshot verification test -id: "screenshot-viewport-001" -name: "Take Viewport Screenshot" -description: "Test taking viewport screenshot and verify functionality" -enabled: true - -target: - url: "https://www.google.com" - -tool: "take_screenshot" -timeout: 30000 - -input: - fullPage: false - - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o" - criteria: - - "Screenshot was successfully captured" - - "Data URL is properly formatted and contains image data" - - "Screenshot shows the viewport content correctly" - - "No errors occurred during screenshot capture" - - "Image data length indicates a valid screenshot was taken" - visual_verification: - enabled: true - capture_before: false - capture_after: true - prompts: - - "Verify the screenshot shows the Google homepage" - - "Check that the screenshot is not empty or corrupted" - - "Confirm the image quality is appropriate for verification" - - "Ensure the screenshot captures the current viewport accurately" - -metadata: - tags: ["screenshot", "viewport", "visual", "verification"] - priority: "high" - timeout: 30000 - retries: 2 - flaky: false \ No newline at end of file diff --git a/eval-server/nodejs/evals/screenshot-verification/visual-comparison-001.yaml b/eval-server/nodejs/evals/screenshot-verification/visual-comparison-001.yaml deleted file mode 100644 index 7434a93e02..0000000000 --- a/eval-server/nodejs/evals/screenshot-verification/visual-comparison-001.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Visual comparison verification test -id: "visual-comparison-001" -name: "Visual Comparison Before and After Action" -description: "Test visual verification by comparing screenshots before and after an action" -enabled: true - -target: - url: "https://www.google.com" - -tool: "action_agent" -timeout: 60000 - -input: - objective: "Take a screenshot, then type 'DevTools testing' in the search box, and take another screenshot to compare" - reasoning: "Testing visual verification workflow with before/after screenshot comparison" - hint: "Use take_screenshot tool before and after performing the search input action" - - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o" - criteria: - - "Initial screenshot was taken before performing any actions" - - "Search text was successfully entered into the search field" - - "Second screenshot was taken after the text input" - - "Visual comparison shows the difference between before and after states" - - "Search field contains the entered text in the final screenshot" - - "Screenshots demonstrate successful action verification workflow" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Compare the before and after screenshots" - - "Verify the search field is empty in the first screenshot" - - "Confirm the search field contains 'DevTools testing' in the second screenshot" - - "Check that the visual changes accurately reflect the performed action" - -metadata: - tags: ["screenshot", "visual-comparison", "action-verification", "before-after"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: false \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/amazon-product-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/amazon-product-001.yaml deleted file mode 100644 index b1544549e4..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/amazon-product-001.yaml +++ /dev/null @@ -1,78 +0,0 @@ -# E-commerce product extraction test (Streamlined) -id: "amazon-product-001" -name: "Extract Amazon Product Details" -description: "Extract product information from an Amazon product page" -enabled: true - -target: - url: "https://www.amazon.com/Obelisk-Climbing-Rustproof-Trellises-Clematis/dp/B0B4SBY6QD/" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 60000 - -input: - schema: - type: "object" - properties: - product: - type: "object" - properties: - title: - type: "string" - brand: - type: "string" - price: - type: "object" - properties: - current: - type: "number" - currency: - type: "string" - rating: - type: "object" - properties: - average: - type: "number" - count: - type: "number" - images: - type: "array" - items: - type: "string" - format: "url" - features: - type: "array" - items: - type: "string" - required: - - "title" - - "price" - availability: - type: "string" - required: - - "product" - instruction: "Extract comprehensive product information including pricing, ratings, and key features" - reasoning: "Testing extraction from a dynamic e-commerce page with complex structure" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Product title is accurate and complete" - - "Price information is current and properly formatted" - - "Rating data includes both average and review count" - - "Image URLs are valid and accessible" - - "Key product features are captured" - - "All URLs are properly resolved (not node IDs)" - -metadata: - tags: ["ecommerce", "amazon", "product", "dynamic"] - priority: "high" - timeout: 60000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/bbc-news-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/bbc-news-001.yaml deleted file mode 100644 index 31ef2883ec..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/bbc-news-001.yaml +++ /dev/null @@ -1,69 +0,0 @@ -# News article extraction test (Streamlined) -id: "bbc-news-001" -name: "Extract BBC News Article" -description: "Extract article content and metadata from a BBC News page" -enabled: true - -target: - url: "https://www.bbc.com/news/technology" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 30000 - -input: - schema: - type: "object" - properties: - headlines: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - summary: - type: "string" - url: - type: "string" - format: "url" - category: - type: "string" - required: - - "title" - mainStory: - type: "object" - properties: - headline: - type: "string" - summary: - type: "string" - url: - type: "string" - format: "url" - required: - - "headlines" - instruction: "Extract the main headlines and featured stories from the BBC Technology news section" - reasoning: "Testing extraction from a news aggregation page with multiple articles" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - include_url: true - criteria: - - "Headlines are current and relevant to technology news" - - "Article summaries provide meaningful context" - - "URLs link to valid BBC news articles" - - "Main story is properly identified" - - "All extracted content is in English" - -metadata: - tags: ["news", "bbc", "aggregation", "dynamic"] - priority: "high" - timeout: 30000 - retries: 2 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/bing-search-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/bing-search-001.yaml deleted file mode 100644 index e9f3b6edb3..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/bing-search-001.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# Bing Search results extraction test -id: "bing-search-001" -name: "Extract Bing Search Results" -description: "Extract search results from Bing search page" -enabled: true - -target: - url: "https://www.bing.com/search?q=web+scraping+best+practices" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 45000 - -input: - schema: - type: "object" - properties: - query: - type: "string" - searchResults: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - url: - type: "string" - format: "url" - snippet: - type: "string" - datePublished: - type: "string" - required: - - "title" - - "url" - - "snippet" - sidebarInfo: - type: "object" - properties: - title: - type: "string" - description: - type: "string" - source: - type: "string" - required: - - "searchResults" - instruction: "Extract search results including titles, URLs, snippets, and any sidebar information from Bing" - reasoning: "Testing extraction from Bing search results with different layout than Google" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Search results match the query intent" - - "Results include valid URLs and meaningful snippets" - - "Sidebar information is extracted when present" - - "No duplicate results in the list" - -metadata: - tags: ["search", "bing", "serp", "dynamic"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/github-repo-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/github-repo-001.yaml deleted file mode 100644 index 5c496c518f..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/github-repo-001.yaml +++ /dev/null @@ -1,66 +0,0 @@ -# Simple structured data test (Streamlined) -id: "github-repo-001" -name: "Extract GitHub Repository Info" -description: "Extract basic repository information from a GitHub page" -enabled: true - -target: - url: "https://github.com/microsoft/TypeScript" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 30000 - -input: - schema: - type: "object" - properties: - name: - type: "string" - description: - type: "string" - language: - type: "string" - stars: - type: "number" - forks: - type: "number" - topics: - type: "array" - items: - type: "string" - readme: - type: "object" - properties: - summary: - type: "string" - required: - - "name" - - "description" - instruction: "Extract repository metadata and basic statistics" - reasoning: "Testing extraction from a well-structured GitHub repository page" - -validation: - type: "hybrid" - snapshot: - exclude_paths: - - "stars" - - "forks" - structure_only: false - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Repository name matches the GitHub page" - - "Description accurately reflects the project purpose" - - "Programming language is correctly identified" - - "Topic tags are relevant to the project" - -metadata: - tags: ["github", "repository", "structured"] - priority: "high" - timeout: 30000 - retries: 1 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/google-flights-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/google-flights-001.yaml deleted file mode 100644 index 981ccbd48d..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/google-flights-001.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# Google Flights search extraction test -id: "google-flights-001" -name: "Extract Google Flights Search Results" -description: "Extract flight options from Google Flights search" -enabled: true - -target: - url: "https://www.google.com/travel/flights/search?tfs=CBwQAhojEgoyMDI1LTEyLTI0agwIAhIIL20vMGQ5anJyBwgBEgNTRk8aIxIKMjAyNS0xMi0zMWoHCAESA1NGT3IMCAISCC9tLzBkOWpyQAFIAXABggELCP___________wGYAQE" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 60000 - -input: - schema: - type: "object" - properties: - searchCriteria: - type: "object" - properties: - origin: - type: "string" - destination: - type: "string" - departureDate: - type: "string" - returnDate: - type: "string" - tripType: - type: "string" - passengers: - type: "number" - flights: - type: "array" - items: - type: "object" - properties: - airline: - type: "string" - flightNumber: - type: "string" - departureTime: - type: "string" - arrivalTime: - type: "string" - duration: - type: "string" - stops: - type: "number" - price: - type: "object" - properties: - amount: - type: "number" - currency: - type: "string" - cabin: - type: "string" - bookingUrl: - type: "string" - format: "url" - legroom: - type: "string" - amenities: - type: "array" - items: - type: "string" - required: - - "airline" - - "departureTime" - - "arrivalTime" - - "price" - priceInsights: - type: "object" - properties: - trend: - type: "string" - recommendation: - type: "string" - averagePrice: - type: "number" - required: - - "flights" - instruction: "Extract flight options including airlines, times, prices, and amenities from Google Flights results" - reasoning: "Testing extraction from complex travel search interface with dynamic pricing" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Flight times are in proper format" - - "Prices are numeric values with currency" - - "Airlines and flight numbers are accurate" - - "Stop information is correctly identified" - - "Duration is in readable format" - -metadata: - tags: ["travel", "flights", "google", "booking"] - priority: "high" - timeout: 60000 - retries: 2 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/google-search-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/google-search-001.yaml deleted file mode 100644 index c1725d481d..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/google-search-001.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# Google Search results extraction test -id: "google-search-001" -name: "Extract Google Search Results" -description: "Extract search results from Google search page" -enabled: true - -target: - url: "https://www.google.com/search?q=chrome+devtools+tutorial" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 45000 - -input: - schema: - type: "object" - properties: - query: - type: "string" - searchResults: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - url: - type: "string" - format: "url" - snippet: - type: "string" - domain: - type: "string" - required: - - "title" - - "url" - - "snippet" - featuredSnippet: - type: "object" - properties: - content: - type: "string" - source: - type: "string" - url: - type: "string" - format: "url" - relatedSearches: - type: "array" - items: - type: "string" - required: - - "searchResults" - instruction: "Extract the top 10 search results with titles, URLs, and snippets. Also extract featured snippet if present and related searches" - reasoning: "Testing extraction from Google search results page with various result types" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Search results are relevant to the query" - - "Each result has a valid title, URL, and snippet" - - "URLs are properly resolved and not node IDs" - - "Related searches are extracted if present" - - "Featured snippet is captured when available" - -metadata: - tags: ["search", "google", "serp", "dynamic"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/homedepot-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/homedepot-001.yaml deleted file mode 100644 index 1d268488a3..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/homedepot-001.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# Home Depot product search extraction test -id: "homedepot-001" -name: "Extract Home Depot Product Search" -description: "Extract product listings from Home Depot search results" -enabled: true - -target: - url: "https://www.homedepot.com/s/power%2520drill" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 60000 - -input: - schema: - type: "object" - properties: - searchQuery: - type: "string" - totalResults: - type: "number" - products: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - brand: - type: "string" - price: - type: "number" - originalPrice: - type: "number" - savings: - type: "number" - rating: - type: "number" - reviewCount: - type: "number" - productUrl: - type: "string" - format: "url" - imageUrl: - type: "string" - format: "url" - availability: - type: "string" - features: - type: "array" - items: - type: "string" - required: - - "name" - - "price" - - "productUrl" - filters: - type: "object" - properties: - brands: - type: "array" - items: - type: "string" - priceRanges: - type: "array" - items: - type: "string" - required: - - "products" - instruction: "Extract product listings from Home Depot search results including prices, ratings, and availability" - reasoning: "Testing extraction from e-commerce search results with product cards and filters" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Products are relevant to the search query" - - "Prices are numeric values in USD" - - "Product URLs link to Home Depot product pages" - - "Ratings are on a 5-star scale" - - "Key product features are captured" - -metadata: - tags: ["ecommerce", "homedepot", "products", "search"] - priority: "high" - timeout: 60000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/macys-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/macys-001.yaml deleted file mode 100644 index 28a2c1056c..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/macys-001.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# Macy's product listing extraction test -id: "macys-001" -name: "Extract Macy's Product Listings" -description: "Extract fashion products from Macy's category page" -enabled: true - -target: - url: "https://www.macys.com/shop/womens-clothing/womens-dresses" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 60000 - -input: - schema: - type: "object" - properties: - category: - type: "string" - totalProducts: - type: "number" - products: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - brand: - type: "string" - currentPrice: - type: "number" - originalPrice: - type: "number" - discount: - type: "string" - colors: - type: "array" - items: - type: "string" - sizes: - type: "array" - items: - type: "string" - rating: - type: "number" - reviewCount: - type: "number" - productUrl: - type: "string" - format: "url" - imageUrl: - type: "string" - format: "url" - promotions: - type: "array" - items: - type: "string" - required: - - "name" - - "brand" - - "currentPrice" - refinements: - type: "object" - properties: - brands: - type: "array" - items: - type: "string" - sizes: - type: "array" - items: - type: "string" - colors: - type: "array" - items: - type: "string" - priceRanges: - type: "array" - items: - type: "string" - required: - - "products" - instruction: "Extract fashion products including prices, sizes, colors, and promotional offers from Macy's" - reasoning: "Testing extraction from fashion e-commerce with complex product attributes" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Products are from the correct category" - - "Prices reflect current and sale prices" - - "Color and size options are captured" - - "Brand names are accurately extracted" - - "Promotional text is included when present" - -metadata: - tags: ["ecommerce", "macys", "fashion", "products"] - priority: "high" - timeout: 60000 - retries: 3 - flaky: true - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/wikipedia-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/wikipedia-001.yaml deleted file mode 100644 index 88983bd32c..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/wikipedia-001.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# Wikipedia article extraction test (Streamlined) -id: "wikipedia-chrome-devtools-001" -name: "Extract Chrome DevTools Wikipedia Article" -description: "Extract structured information from the Chrome DevTools Wikipedia page" -enabled: true - -target: - url: "https://en.wikipedia.org/wiki/Chrome_DevTools" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 45000 - -input: - schema: - type: "object" - properties: - title: - type: "string" - summary: - type: "string" - tableOfContents: - type: "array" - items: - type: "string" - infobox: - type: "object" - properties: - developer: - type: "string" - initialRelease: - type: "string" - operatingSystem: - type: "string" - license: - type: "string" - externalLinks: - type: "array" - items: - type: "object" - properties: - text: - type: "string" - url: - type: "string" - format: "url" - required: - - "title" - - "summary" - instruction: "Extract the main article information including title, summary, table of contents, and infobox details" - reasoning: "Testing extraction from a stable, well-structured Wikipedia page" - -validation: - type: "hybrid" - snapshot: - exclude_paths: - - "externalLinks[*].url" - structure_only: false - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Article title matches the Wikipedia page title" - - "Summary captures the main description of Chrome DevTools" - - "Table of contents includes major sections" - - "Infobox contains key technical details" - - "External links are properly resolved URLs" - -metadata: - tags: ["wikipedia", "documentation", "stable"] - priority: "high" - timeout: 45000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/streamlined-schema-extractor/wikipedia-search-001.yaml b/eval-server/nodejs/evals/streamlined-schema-extractor/wikipedia-search-001.yaml deleted file mode 100644 index c432c20d4f..0000000000 --- a/eval-server/nodejs/evals/streamlined-schema-extractor/wikipedia-search-001.yaml +++ /dev/null @@ -1,77 +0,0 @@ -# Wikipedia search results extraction test -id: "wikipedia-search-001" -name: "Extract Wikipedia Search Results" -description: "Extract search results from Wikipedia search" -enabled: true - -target: - url: "https://en.wikipedia.org/w/index.php?search=artificial+intelligence&title=Special:Search" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "extract_schema_streamlined" -timeout: 30000 - -input: - schema: - type: "object" - properties: - searchTerm: - type: "string" - resultCount: - type: "number" - searchResults: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - url: - type: "string" - format: "url" - snippet: - type: "string" - category: - type: "string" - wordCount: - type: "number" - lastEdited: - type: "string" - required: - - "title" - - "url" - - "snippet" - suggestedArticles: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - url: - type: "string" - format: "url" - required: - - "searchResults" - instruction: "Extract Wikipedia search results including article titles, URLs, snippets, and metadata like word count or last edit date" - reasoning: "Testing extraction from Wikipedia's internal search with rich metadata" - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4.1-mini" - temperature: 0.3 - criteria: - - "Search results are Wikipedia articles" - - "Each result has a valid Wikipedia URL" - - "Snippets contain relevant content highlights" - - "Metadata like word count is extracted when available" - -metadata: - tags: ["search", "wikipedia", "encyclopedia"] - priority: "high" - timeout: 30000 - retries: 2 - flaky: false - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/booking-001.yaml b/eval-server/nodejs/evals/web-task-agent/booking-001.yaml deleted file mode 100644 index 8a99d17a4e..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/booking-001.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Hotel Search Workflow - Web Task Agent -id: "booking-001" -name: "Hotel Search Workflow" -description: "Test web task agent orchestrating complex multi-step booking search" -enabled: true - -target: - url: "https://www.booking.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for hotels in San Francisco for 2 adults, check-in March 15, check-out March 17" - reasoning: "Customer is looking for travel booking" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully searched for hotels in San Francisco" - - "Results show hotels available for March 15-17 dates" - - "Guest count of 2 adults is reflected in the search results" - - "Returned multiple hotel options with relevant details" - - "Each hotel includes essential information (name, price, location)" - - "Results are presented in a clear, readable format" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify hotel search results are displayed for San Francisco" - - "Check that dates March 15-17 are correctly selected" - - "Confirm guest count shows 2 adults" - - "Ensure search results show hotels with availability for specified dates" - -metadata: - tags: ["web-task", "booking", "workflow", "multi-step", "travel", "complex"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/ecommerce-001.yaml b/eval-server/nodejs/evals/web-task-agent/ecommerce-001.yaml deleted file mode 100644 index 338f4643d2..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/ecommerce-001.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# E-commerce web task evaluation (matches DevTools test case) -id: "ecommerce-001" -name: "E-commerce Product Search" -description: "Test web task agent handling product search on shopping site" -enabled: true - -target: - url: "https://www.amazon.com" - -tool: "web_task_agent" -timeout: 90000 - -input: - task: "Search Amazon for \"wireless headphones\" and find products under $100" - reasoning: "Testing e-commerce search workflow with price filtering" - context: "User wants to find wireless headphones with specific price constraint" - extraction_schema: - type: "object" - properties: - products: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - price: - type: "string" - rating: - type: "string" - url: - type: "string" - - -validation: - type: "hybrid" - llm_judge: - model: "gpt-4o" - criteria: - - "Successfully navigated to product search" - - "Applied appropriate filters correctly" - - "Extracted product details accurately" - - "Provided meaningful comparison of features" - - "Stayed within specified price range" - snapshot: - structure_only: true - exclude_paths: - - "timestamp" - - "sessionId" - -metadata: - tags: ["web-task", "multi-step", "ecommerce", "search"] - priority: "high" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/error-001.yaml b/eval-server/nodejs/evals/web-task-agent/error-001.yaml deleted file mode 100644 index 1831a14e91..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/error-001.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Error Recovery Workflow - Web Task Agent -id: "error-001" -name: "Error Recovery Workflow" -description: "Test web task agent handling action_agent failures and retry logic" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"nonexistent test query 12345\" and handle any issues that arise" - reasoning: "Customer is asking for this response" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Attempted to search for the unusual query \"nonexistent test query 12345\"" - - "Either found some results OR provided clear explanation why no results were found" - - "Response handles the edge case gracefully without errors" - - "If no results found, suggested alternative actions or explanations" - - "Maintained professional tone despite unusual request" - - "Final output is coherent and helpful to the user" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Check if search was attempted despite unusual query" - - "Verify error handling did not break the page interaction" - - "Confirm agent attempted to complete the task or provided clear error info" - - "Ensure page is still functional after error recovery attempts" - -metadata: - tags: ["web-task", "error-recovery", "retry", "orchestration", "robustness"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/extract-001.yaml b/eval-server/nodejs/evals/web-task-agent/extract-001.yaml deleted file mode 100644 index e836aa0c41..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/extract-001.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# Structured Data Extraction - Web Task Agent -id: "extract-001" -name: "Structured Data Extraction" -description: "Test web task agent extracting structured data from search results" -enabled: true - -target: - url: "https://news.ycombinator.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Extract the top 5 Hacker News stories with their titles, scores, and comment counts" - reasoning: "User is looking to understand the top stories on Hacker News" - extraction_schema: - type: "object" - properties: - stories: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - score: - type: "number" - comments: - type: "number" - url: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully returned exactly 5 Hacker News stories in structured text format" - - "Each story is numbered (1., 2., 3., 4., 5.) with title, score, comments, and URL" - - "Results are presented in readable text format similar to the example provided" - - "Response includes all required fields: title, score, comments count, URL" - - "Maintained proper orchestration pattern throughout the extraction process" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Hacker News homepage is loaded and displaying stories" - - "Check that top stories are visible with scores and comment counts" - - "Confirm story titles and metadata are clearly displayed" - - "Ensure page structure allows for data extraction" - -metadata: - tags: ["web-task", "data-extraction", "structured-data", "hackernews", "schema"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/finance-001.yaml b/eval-server/nodejs/evals/web-task-agent/finance-001.yaml deleted file mode 100644 index 2c661ede9d..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/finance-001.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Stock Information Research - Web Task Agent -id: "finance-001" -name: "Stock Information Research" -description: "Test extracting stock prices and financial information" -enabled: true - -target: - url: "https://finance.yahoo.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for Apple (AAPL) stock information and extract current price, market cap, and recent performance" - reasoning: "Users need automated financial data collection for investment decisions" - extraction_schema: - type: "object" - properties: - stock_info: - type: "object" - properties: - symbol: - type: "string" - company_name: - type: "string" - current_price: - type: "string" - change: - type: "string" - change_percent: - type: "string" - market_cap: - type: "string" - pe_ratio: - type: "string" - volume: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully found Apple (AAPL) stock information" - - "Current stock price is clearly stated" - - "Market cap information is included" - - "Price change and percentage change are provided" - - "Additional metrics (PE ratio, volume) included when available" - - "Financial data is current and presented in readable text format (not JSON)" - - "Stock information is well-organized and easy to understand" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Yahoo Finance shows Apple (AAPL) stock page" - - "Check that current stock price and change are visible" - - "Confirm market cap and trading volume are displayed" - - "Ensure financial metrics and charts are shown" - -metadata: - tags: ["web-task", "finance", "stocks", "yahoo-finance", "investment", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/flight-001.yaml b/eval-server/nodejs/evals/web-task-agent/flight-001.yaml deleted file mode 100644 index f74b255ef1..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/flight-001.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Complex Flight Search - Web Task Agent -id: "flight-001" -name: "Complex Flight Search" -description: "Test web task agent handling complex flight search with multiple criteria" -enabled: true - -target: - url: "https://www.kayak.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for round-trip flights from Seattle (SEA) to Tokyo (NRT) departing March 20, returning March 30" - reasoning: "Customer is looking for finding the best flight options" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully found round-trip flights from Seattle (SEA) to Tokyo (NRT)" - - "Flight results show March 20 departure date" - - "Flight results show March 30 return date" - - "Returned multiple flight options with airlines and prices" - - "Each flight includes essential details (times, airlines, prices)" - - "Results clearly distinguish between outbound and return flights" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify flight search results are displayed" - - "Check SEA to NRT route is correctly selected" - - "Confirm dates March 20 departure and March 30 return" - - "Ensure flight options are showing with prices and airlines" - -metadata: - tags: ["web-task", "flight", "travel", "multi-step", "kayak", "round-trip"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/food-001.yaml b/eval-server/nodejs/evals/web-task-agent/food-001.yaml deleted file mode 100644 index 382b470aa2..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/food-001.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Restaurant Search and Menu Extraction - Web Task Agent -id: "food-001" -name: "Restaurant Search and Menu Extraction" -description: "Test searching restaurants and extracting menu information" -enabled: true - -target: - url: "https://www.yelp.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"Italian restaurants near me\" in San Francisco and extract restaurant details" - reasoning: "Users want to quickly compare restaurants, menus, and reviews" - extraction_schema: - type: "object" - properties: - restaurants: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - rating: - type: "string" - price_range: - type: "string" - cuisine: - type: "string" - address: - type: "string" - phone: - type: "string" - hours: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully found Italian restaurants in San Francisco" - - "Each restaurant includes name, rating, and price range" - - "Location/address information is provided for each restaurant" - - "Contact details (phone/hours) included when available" - - "All restaurants listed serve Italian cuisine" - - "Results are presented in clear, structured text format (not JSON)" - - "Restaurants are numbered or organized clearly for easy comparison" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Yelp search results for Italian restaurants" - - "Check that restaurants show ratings and price ranges" - - "Confirm location filter shows San Francisco results" - - "Ensure restaurant listings include contact information" - -metadata: - tags: ["web-task", "restaurants", "yelp", "food", "local-search", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/iframe-001.yaml b/eval-server/nodejs/evals/web-task-agent/iframe-001.yaml deleted file mode 100644 index a9234e52f7..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/iframe-001.yaml +++ /dev/null @@ -1,83 +0,0 @@ -# ANA Airlines Iframe Content Extraction - Web Task Agent -id: "iframe-001" -name: "ANA Airlines Iframe Content Extraction" -description: "Test web task agent handling iframe-heavy airline booking sites like ANA Airlines" -enabled: true - -target: - url: "https://aswbe.ana.co.jp/webapps/reservation/flight-search?CONNECTION_KIND=SEA&LANG=en&hiddenSearchMode=ROUND_TRIP&departureDate:field=20260320&returnDate:field=20260330&departureAirportCode:field=SEA&arrivalAirportCode:field=NRT&adultCount=1&youngAdultCount=0&childCount=0&infantCount=0&boardingClass=INTY001&searchFlag=1" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Navigate the ANA Airlines flight search page and extract available flight options from Seattle (SEA) to Tokyo Narita (NRT) for March 20-30, 2026. Handle any iframe content and booking interface elements." - reasoning: "Testing iframe content extraction and complex airline booking site navigation" - extraction_schema: - type: "object" - properties: - flights: - type: "array" - items: - type: "object" - properties: - flight_number: - type: "string" - airline: - type: "string" - departure_time: - type: "string" - arrival_time: - type: "string" - departure_date: - type: "string" - arrival_date: - type: "string" - duration: - type: "string" - aircraft: - type: "string" - price: - type: "string" - cabin_class: - type: "string" - stops: - type: "string" - booking_interface_status: - type: "string" - iframe_content_found: - type: "boolean" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully navigated ANA Airlines booking interface" - - "Handled iframe content correctly (iframe_content_found should be true if iframes detected)" - - "Extracted flight information from ANA flight search results" - - "Flight details include ANA flight numbers and accurate route (SEA to NRT)" - - "Extracted pricing information in appropriate currency" - - "Handled any booking interface elements, popups, or navigation flows" - - "Results show flights for the correct dates (March 20-30, 2026)" - - "Successfully demonstrated iframe content extraction capabilities" - - "Booking interface status indicates successful page interaction" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify ANA Airlines flight search page loaded correctly" - - "Check that search parameters show SEA to NRT route" - - "Confirm flight results are displayed (may be in iframes)" - - "Ensure booking interface elements are functional" - - "Verify flight information is accessible and extractable" - -metadata: - tags: ["web-task", "iframe", "ana-airlines", "complex-booking", "international-flight", "airline-specific"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/jobs-001.yaml b/eval-server/nodejs/evals/web-task-agent/jobs-001.yaml deleted file mode 100644 index 7a6caa8cee..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/jobs-001.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Job Search Workflow - Web Task Agent -id: "jobs-001" -name: "Job Search Workflow" -description: "Test web task agent orchestrating job search on LinkedIn" -enabled: true - -target: - url: "https://www.linkedin.com/jobs" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"Software Engineer\" jobs in \"San Francisco\" and extract details for the first 5 results" - reasoning: "User wants to find job opportunities in tech industry" - extraction_schema: - type: "object" - properties: - jobs: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - company: - type: "string" - location: - type: "string" - salary: - type: "string" - description: - type: "string" - url: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Either used construct_direct_url for LinkedIn job search OR used traditional form interaction" - - "If using direct URL: constructed proper LinkedIn job search URL with keywords and location" - - "If using forms: delegated keyword and location input to action_agent" - - "Extracted job listings using extract_data" - - "Returned structured job data in readable text format (not JSON)" - - "Each job listing includes title, company, location, and other relevant fields" - - "Results are numbered or organized clearly for easy reading" - - "Demonstrated proper workflow orchestration for job search" - - "Never used direct browser interaction tools" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify LinkedIn job search results are displayed" - - "Check that search shows Software Engineer jobs in San Francisco" - - "Confirm job listings include company names and titles" - - "Ensure at least 5 job results are visible" - -metadata: - tags: ["web-task", "jobs", "linkedin", "search", "career", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/learning-001.yaml b/eval-server/nodejs/evals/web-task-agent/learning-001.yaml deleted file mode 100644 index 1e4c761b19..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/learning-001.yaml +++ /dev/null @@ -1,69 +0,0 @@ -# Online Course Search - Web Task Agent -id: "learning-001" -name: "Online Course Search" -description: "Test searching and extracting course information from learning platforms" -enabled: true - -target: - url: "https://www.coursera.org" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"Machine Learning\" courses and extract details for top 5 results" - reasoning: "Users want to compare courses across platforms for learning decisions" - extraction_schema: - type: "object" - properties: - courses: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - instructor: - type: "string" - university: - type: "string" - rating: - type: "string" - duration: - type: "string" - price: - type: "string" - description: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully found Machine Learning courses on Coursera" - - "Returned details for top 5 courses as requested" - - "Each course includes title, instructor, university, and rating" - - "Duration and pricing information included for each course" - - "Course descriptions or key topics are provided" - - "Results are presented in structured text format (not JSON)" - - "Courses are numbered (1-5) and well-organized for easy comparison" - - "Each course entry is clearly formatted and readable" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Coursera search results for Machine Learning" - - "Check that courses show titles, instructors, and ratings" - - "Confirm course details include duration and pricing" - - "Ensure search results are relevant to Machine Learning" - -metadata: - tags: ["web-task", "education", "coursera", "courses", "learning", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/nav-001.yaml b/eval-server/nodejs/evals/web-task-agent/nav-001.yaml deleted file mode 100644 index bff519fafe..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/nav-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Site Navigation Workflow - Web Task Agent -id: "nav-001" -name: "Site Navigation Workflow" -description: "Test web task agent orchestrating navigation between different sections of a site" -enabled: true - -target: - url: "https://www.wikipedia.org" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 90000 - -input: - task: "Navigate to the Wikipedia homepage, search for \"artificial intelligence\", and find information about machine learning" - reasoning: "User is looking to explore Wikipedia content through structured navigation" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Orchestrated Wikipedia search via action_agent calls" - - "Navigated to artificial intelligence article through action_agent" - - "Located machine learning section via action_agent coordination" - - "Extracted relevant information about machine learning" - - "Demonstrated multi-step navigation workflow" - - "Maintained orchestration pattern throughout navigation" - - "Provided structured summary of found information" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify navigation reached artificial intelligence Wikipedia page" - - "Check that machine learning section or content is visible" - - "Confirm successful navigation through multiple page sections" - - "Ensure content related to machine learning is displayed" - -metadata: - tags: ["web-task", "navigation", "multi-step", "wikipedia", "content-exploration"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/news-001.yaml b/eval-server/nodejs/evals/web-task-agent/news-001.yaml deleted file mode 100644 index 4c29aed665..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/news-001.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# News Article Aggregation - Web Task Agent -id: "news-001" -name: "News Article Aggregation" -description: "Test aggregating news headlines and summaries from news sites" -enabled: true - -target: - url: "https://news.ycombinator.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Extract the top 10 Hacker News stories with titles, scores, and first few comments" - reasoning: "Users want automated news monitoring for research and awareness" - extraction_schema: - type: "object" - properties: - articles: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - score: - type: "number" - comments_count: - type: "number" - url: - type: "string" - top_comment: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully extracted 10 Hacker News stories as requested" - - "Each story includes title, score, and comment count" - - "URLs are provided for each story" - - "Stories appear to be from the current top/front page" - - "Results are presented in clear, numbered text format (1-10), not JSON" - - "All required fields are present and properly formatted in readable text" - - "Each story is clearly separated and easy to read" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Hacker News stories are visible with scores" - - "Check that story titles and comment counts are shown" - - "Confirm top stories section is properly displayed" - - "Ensure story metadata is accessible for extraction" - -metadata: - tags: ["web-task", "news", "hackernews", "aggregation", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/realestate-001.yaml b/eval-server/nodejs/evals/web-task-agent/realestate-001.yaml deleted file mode 100644 index 5fd824ea79..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/realestate-001.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# Real Estate Property Search - Web Task Agent -id: "realestate-001" -name: "Real Estate Property Search" -description: "Test property search workflow on real estate platforms" -enabled: true - -target: - url: "https://www.zillow.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for houses for sale in Austin, Texas under $500k and extract property details" - reasoning: "User wants to find affordable housing options in a specific location" - extraction_schema: - type: "object" - properties: - properties: - type: "array" - items: - type: "object" - properties: - address: - type: "string" - price: - type: "string" - bedrooms: - type: "number" - bathrooms: - type: "number" - sqft: - type: "string" - lot_size: - type: "string" - year_built: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Orchestrated location search via action_agent" - - "Delegated price filter setting to action_agent" - - "Coordinated property type selection through action_agent" - - "Applied search filters through proper action_agent calls" - - "Extracted property listings with extract_data" - - "Returned structured property data in readable text format (not JSON)" - - "Each property includes address, price, bedrooms, bathrooms, and other key details" - - "Properties are clearly numbered or organized for easy comparison" - - "Demonstrated complex real estate search workflow orchestration" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Zillow search results for Austin, Texas properties" - - "Check that properties shown are under $500k" - - "Confirm property listings show price, beds, baths info" - - "Ensure search results match the specified criteria" - -metadata: - tags: ["web-task", "real-estate", "zillow", "property-search", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/scroll-001.yaml b/eval-server/nodejs/evals/web-task-agent/scroll-001.yaml deleted file mode 100644 index 12a986fc7d..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/scroll-001.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# Infinite Scroll Content Loading - Web Task Agent -id: "scroll-001" -name: "Infinite Scroll Content Loading" -description: "Test web task agent handling infinite scroll pages to load more content" -enabled: true - -target: - url: "https://twitter.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Scroll down the Twitter feed to load at least 20 tweets and extract their content" - reasoning: "Testing infinite scroll functionality for dynamic content loading" - extraction_schema: - type: "object" - properties: - tweets: - type: "array" - items: - type: "object" - properties: - author: - type: "string" - content: - type: "string" - likes: - type: "string" - retweets: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully used scroll_page tool to scroll down the page" - - "Loaded additional content through scrolling actions" - - "Extracted at least 20 tweets from the feed" - - "Each tweet includes author and content information" - - "Demonstrated proper handling of dynamically loaded content" - - "Results are presented in clear, numbered text format" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify initial Twitter feed is loaded" - - "Check that scrolling action loaded additional tweets" - - "Confirm at least 20 tweets are visible after scrolling" - - "Ensure page scrolled down significantly from initial position" - -metadata: - tags: ["web-task", "scrolling", "infinite-scroll", "dynamic-content", "twitter"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/scroll-002.yaml b/eval-server/nodejs/evals/web-task-agent/scroll-002.yaml deleted file mode 100644 index dce0156a31..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/scroll-002.yaml +++ /dev/null @@ -1,65 +0,0 @@ -# Product Review Scrolling - Web Task Agent -id: "scroll-002" -name: "Product Review Scrolling" -description: "Test scrolling to load more product reviews on e-commerce sites" -enabled: true - -target: - url: "https://www.amazon.com/dp/B09B8V1LZ3" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Scroll down to the reviews section and load more reviews by scrolling, then extract review details" - reasoning: "Users need to see multiple reviews beyond initial visible ones" - extraction_schema: - type: "object" - properties: - reviews: - type: "array" - items: - type: "object" - properties: - rating: - type: "string" - title: - type: "string" - author: - type: "string" - date: - type: "string" - verified: - type: "boolean" - content: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Used scroll_page tool to navigate to reviews section" - - "Scrolled within reviews area to load additional reviews" - - "Extracted multiple product reviews with ratings" - - "Each review includes rating, author, and content" - - "Successfully handled lazy-loaded review content" - - "Presented reviews in structured, readable format" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Amazon product page is loaded" - - "Check that page scrolled to reviews section" - - "Confirm additional reviews loaded after scrolling" - - "Ensure review content is fully visible" - -metadata: - tags: ["web-task", "scrolling", "reviews", "amazon", "e-commerce"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/scroll-003.yaml b/eval-server/nodejs/evals/web-task-agent/scroll-003.yaml deleted file mode 100644 index df7eaba01a..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/scroll-003.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# News Article Progressive Loading - Web Task Agent -id: "scroll-003" -name: "News Article Progressive Loading" -description: "Test scrolling through news sites that load articles progressively" -enabled: true - -target: - url: "https://medium.com/topic/technology" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Scroll down to load more technology articles and extract titles and authors for at least 15 articles" - reasoning: "Testing progressive content loading on news/blog platforms" - extraction_schema: - type: "object" - properties: - articles: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - author: - type: "string" - reading_time: - type: "string" - preview: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Used scroll_page tool multiple times to load content" - - "Successfully loaded at least 15 articles through scrolling" - - "Extracted article titles and author information" - - "Handled Medium's progressive loading mechanism" - - "Articles are from technology topic as requested" - - "Results presented in clear, numbered format" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Medium technology page is loaded" - - "Check that initial articles are visible" - - "Confirm scrolling loaded additional articles" - - "Ensure at least 15 articles are visible after scrolling" - -metadata: - tags: ["web-task", "scrolling", "progressive-loading", "medium", "articles"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/scroll-004.yaml b/eval-server/nodejs/evals/web-task-agent/scroll-004.yaml deleted file mode 100644 index e9b3534c34..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/scroll-004.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# Search Results Infinite Scroll - Web Task Agent -id: "scroll-004" -name: "Search Results Infinite Scroll" -description: "Test handling search results that use infinite scroll instead of pagination" -enabled: true - -target: - url: "https://www.pinterest.com/search/pins/?q=web%20design" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"web design\" pins and scroll to load at least 30 results, then extract pin details" - reasoning: "Testing infinite scroll on visual search platforms" - extraction_schema: - type: "object" - properties: - pins: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - description: - type: "string" - saves: - type: "string" - source: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully performed search for \"web design\" pins" - - "Used scroll_page tool to trigger infinite scroll loading" - - "Loaded at least 30 pins through scrolling actions" - - "Extracted pin titles and metadata" - - "Handled Pinterest's masonry layout and lazy loading" - - "Results are well-organized and readable" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Pinterest search results for web design" - - "Check initial pins are displayed" - - "Confirm scrolling loaded many more pins" - - "Ensure grid layout shows 30+ pins after scrolling" - -metadata: - tags: ["web-task", "scrolling", "infinite-scroll", "pinterest", "visual-search"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/scroll-005.yaml b/eval-server/nodejs/evals/web-task-agent/scroll-005.yaml deleted file mode 100644 index 47c876962a..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/scroll-005.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Google Flights Scroll and Show More - Web Task Agent -id: "scroll-005" -name: "Google Flights Scroll and Show More" -description: "Test scrolling and clicking \"Show more flights\" button on Google Flights to load additional flight options" -enabled: true - -target: - url: "https://www.google.com/travel/flights?sca_esv=646eedf97dcc8cf2&source=flun&uitype=cuAA&hl=en&gl=us&curr=USD&tfs=CAEQAhoeEgoyMDI2LTAzLTIwagcIARIDU0VBcgcIARIDTlJUGh4SCjIwMjYtMDMtMzBqBwgBEgNOUlRyBwgBEgNTRUF6aENqUklhVFJJTVVwVlZVOXpNakJCUTJodGVFRkNSeTB0TFMwdExTMHRjR3BpYjI4eE0wRkJRVUZCUjJoc1lsWlZRV2RYUlZsQkVnTmpTMFVhQ3dqUXNnVVFBaG9EVlZORU9EQncwTElG" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Extract the initial flight results, then scroll down and click \"Show more flights\" button to load additional flights. Extract at least 20 total flight options from Seattle to Tokyo." - reasoning: "Testing combination of scrolling and button clicking to load more flight results on Google Flights" - extraction_schema: - type: "object" - properties: - flights: - type: "array" - items: - type: "object" - properties: - airline: - type: "string" - departure_time: - type: "string" - arrival_time: - type: "string" - duration: - type: "string" - stops: - type: "string" - price: - type: "string" - aircraft: - type: "string" - total_flights_found: - type: "number" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully extracted initial flight results from Google Flights" - - "Used scroll_page tool to scroll down the flight results list" - - "Located and clicked \"Show more flights\" button using action_agent" - - "Loaded additional flight options beyond the initial set" - - "Extracted at least 20 total flights from Seattle (SEA) to Tokyo (NRT)" - - "Each flight includes airline, times, duration, stops, and price" - - "Flights are for the correct dates (March 20-30, 2026)" - - "Results are presented in clear, numbered format" - - "Successfully combined scrolling and clicking actions to load more content" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Google Flights page shows SEA to NRT flights" - - "Check that initial flight results are displayed" - - "Confirm scrolling occurred and \"Show more flights\" button was visible" - - "Ensure additional flights loaded after clicking the button" - - "Verify at least 20 flight options are now visible" - -metadata: - tags: ["web-task", "scrolling", "google-flights", "click-action", "load-more", "travel"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/search-001.yaml b/eval-server/nodejs/evals/web-task-agent/search-001.yaml deleted file mode 100644 index da3a4eb5fd..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/search-001.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Basic web task search evaluation (matches DevTools test case) -id: "search-001" -name: "Site-Specific Search Task" -description: "Test web task agent orchestrating a search workflow on a specific site" -enabled: true - -target: - url: "chrome://new-tab-page" - -tool: "web_task_agent" -timeout: 60000 - -input: - task: "Search Google for \"Chrome DevTools automation\" and extract the top 3 search results" - reasoning: "Testing basic site-specific search workflow orchestration" - context: "Need to demonstrate web_task_agent can coordinate multiple action_agent calls for a complete search workflow" - - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o" - criteria: - - "Successfully returned exactly 3 search results in structured text format" - - "Each result is numbered (1., 2., 3.) and contains a title related to \"Chrome DevTools automation\"" - - "Each result includes a URL in the format \"URL: [link]\"" - - "Results are presented in a clear, readable text format (not JSON)" - - "Response includes a brief summary or conclusion statement" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify search was completed and results page is showing" - - "Check that search results are related to \"Chrome DevTools automation\"" - - "Confirm at least 3 search results are visible on the page" - - "Ensure the search workflow was completed successfully" - -metadata: - tags: ["web-task", "orchestration", "search", "workflow", "google", "basic"] - priority: "normal" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/social-001.yaml b/eval-server/nodejs/evals/web-task-agent/social-001.yaml deleted file mode 100644 index a35ebfd99f..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/social-001.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# Social Media Content Extraction - Web Task Agent -id: "social-001" -name: "Social Media Content Extraction" -description: "Test extracting trending topics and posts from social media" -enabled: true - -target: - url: "https://twitter.com/explore" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Extract the top 5 trending topics from Twitter/X explore page" - reasoning: "User wants to stay updated on current trends" - extraction_schema: - type: "object" - properties: - trends: - type: "array" - items: - type: "object" - properties: - topic: - type: "string" - posts_count: - type: "string" - category: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully accessed Twitter/X explore page and found trending topics" - - "Returned exactly 5 trending topics as requested" - - "Each topic includes the trend name/hashtag" - - "Post counts or metrics are included when available" - - "Topics are current/recent trends (not outdated)" - - "Results are presented in clear, numbered text format (not JSON)" - - "Each trend is properly numbered (1., 2., 3., etc.) for readability" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Twitter/X explore page is loaded" - - "Check that trending topics section is visible" - - "Confirm trending topics show names and post counts" - - "Ensure page shows current trending content" - -metadata: - tags: ["web-task", "social-media", "twitter", "trends", "extraction", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-booking-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-booking-001.yaml deleted file mode 100644 index a2842b6fb8..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-booking-001.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Hotel Search Workflow - Web Task Agent -id: "web-task-agent-booking-001" -name: "Hotel Search Workflow" -description: "Test web task agent orchestrating complex multi-step booking search" -enabled: true - -target: - url: "https://www.booking.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for hotels in San Francisco for 2 adults, check-in March 15, check-out March 17" - reasoning: "Customer is looking for travel booking" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully searched for hotels in San Francisco" - - "Results show hotels available for March 15-17 dates" - - "Guest count of 2 adults is reflected in the search results" - - "Returned multiple hotel options with relevant details" - - "Each hotel includes essential information (name, price, location)" - - "Results are presented in a clear, readable format" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify hotel search results are displayed for San Francisco" - - "Check that dates March 15-17 are correctly selected" - - "Confirm guest count shows 2 adults" - - "Ensure search results show hotels with availability for specified dates" - -metadata: - tags: ["web-task", "booking", "workflow", "multi-step", "travel", "complex"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-ecommerce-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-ecommerce-001.yaml deleted file mode 100644 index a6b97359bd..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-ecommerce-001.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# E-commerce web task evaluation (matches DevTools test case) -id: "web-task-agent-ecommerce-001" -name: "E-commerce Product Search" -description: "Test web task agent handling product search on shopping site" -enabled: true - -target: - url: "https://www.amazon.com" - -tool: "web_task_agent" -timeout: 90000 - -input: - task: "Search Amazon for \"wireless headphones\" and find products under $100" - reasoning: "Testing e-commerce search workflow with price filtering" - context: "User wants to find wireless headphones with specific price constraint" - extraction_schema: - type: "object" - properties: - products: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - price: - type: "string" - rating: - type: "string" - url: - type: "string" - - -validation: - type: "hybrid" - llm_judge: - model: "gpt-4o" - criteria: - - "Successfully navigated to product search" - - "Applied appropriate filters correctly" - - "Extracted product details accurately" - - "Provided meaningful comparison of features" - - "Stayed within specified price range" - snapshot: - structure_only: true - exclude_paths: - - "timestamp" - - "sessionId" - -metadata: - tags: ["web-task", "multi-step", "ecommerce", "search"] - priority: "high" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-error-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-error-001.yaml deleted file mode 100644 index cc5c7df3b1..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-error-001.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Error Recovery Workflow - Web Task Agent -id: "web-task-agent-error-001" -name: "Error Recovery Workflow" -description: "Test web task agent handling action_agent failures and retry logic" -enabled: true - -target: - url: "https://www.google.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"nonexistent test query 12345\" and handle any issues that arise" - reasoning: "Customer is asking for this response" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Attempted to search for the unusual query \"nonexistent test query 12345\"" - - "Either found some results OR provided clear explanation why no results were found" - - "Response handles the edge case gracefully without errors" - - "If no results found, suggested alternative actions or explanations" - - "Maintained professional tone despite unusual request" - - "Final output is coherent and helpful to the user" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Check if search was attempted despite unusual query" - - "Verify error handling did not break the page interaction" - - "Confirm agent attempted to complete the task or provided clear error info" - - "Ensure page is still functional after error recovery attempts" - -metadata: - tags: ["web-task", "error-recovery", "retry", "orchestration", "robustness"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-extract-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-extract-001.yaml deleted file mode 100644 index 14eadcb8c1..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-extract-001.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# Structured Data Extraction - Web Task Agent -id: "web-task-agent-extract-001" -name: "Structured Data Extraction" -description: "Test web task agent extracting structured data from search results" -enabled: true - -target: - url: "https://news.ycombinator.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Extract the top 5 Hacker News stories with their titles, scores, and comment counts" - reasoning: "User is looking to understand the top stories on Hacker News" - extraction_schema: - type: "object" - properties: - stories: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - score: - type: "number" - comments: - type: "number" - url: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully returned exactly 5 Hacker News stories in structured text format" - - "Each story is numbered (1., 2., 3., 4., 5.) with title, score, comments, and URL" - - "Results are presented in readable text format similar to the example provided" - - "Response includes all required fields: title, score, comments count, URL" - - "Maintained proper orchestration pattern throughout the extraction process" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Hacker News homepage is loaded and displaying stories" - - "Check that top stories are visible with scores and comment counts" - - "Confirm story titles and metadata are clearly displayed" - - "Ensure page structure allows for data extraction" - -metadata: - tags: ["web-task", "data-extraction", "structured-data", "hackernews", "schema"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-finance-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-finance-001.yaml deleted file mode 100644 index 8f7a2b0d21..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-finance-001.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Stock Information Research - Web Task Agent -id: "web-task-agent-finance-001" -name: "Stock Information Research" -description: "Test extracting stock prices and financial information" -enabled: true - -target: - url: "https://finance.yahoo.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for Apple (AAPL) stock information and extract current price, market cap, and recent performance" - reasoning: "Users need automated financial data collection for investment decisions" - extraction_schema: - type: "object" - properties: - stock_info: - type: "object" - properties: - symbol: - type: "string" - company_name: - type: "string" - current_price: - type: "string" - change: - type: "string" - change_percent: - type: "string" - market_cap: - type: "string" - pe_ratio: - type: "string" - volume: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully found Apple (AAPL) stock information" - - "Current stock price is clearly stated" - - "Market cap information is included" - - "Price change and percentage change are provided" - - "Additional metrics (PE ratio, volume) included when available" - - "Financial data is current and presented in readable text format (not JSON)" - - "Stock information is well-organized and easy to understand" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Yahoo Finance shows Apple (AAPL) stock page" - - "Check that current stock price and change are visible" - - "Confirm market cap and trading volume are displayed" - - "Ensure financial metrics and charts are shown" - -metadata: - tags: ["web-task", "finance", "stocks", "yahoo-finance", "investment", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-flight-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-flight-001.yaml deleted file mode 100644 index a17883fb68..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-flight-001.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Complex Flight Search - Web Task Agent -id: "web-task-agent-flight-001" -name: "Complex Flight Search" -description: "Test web task agent handling complex flight search with multiple criteria" -enabled: true - -target: - url: "https://www.kayak.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for round-trip flights from Seattle (SEA) to Tokyo (NRT) departing March 20, returning March 30" - reasoning: "Customer is looking for finding the best flight options" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully found round-trip flights from Seattle (SEA) to Tokyo (NRT)" - - "Flight results show March 20 departure date" - - "Flight results show March 30 return date" - - "Returned multiple flight options with airlines and prices" - - "Each flight includes essential details (times, airlines, prices)" - - "Results clearly distinguish between outbound and return flights" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify flight search results are displayed" - - "Check SEA to NRT route is correctly selected" - - "Confirm dates March 20 departure and March 30 return" - - "Ensure flight options are showing with prices and airlines" - -metadata: - tags: ["web-task", "flight", "travel", "multi-step", "kayak", "round-trip"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-food-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-food-001.yaml deleted file mode 100644 index 32ee64609d..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-food-001.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Restaurant Search and Menu Extraction - Web Task Agent -id: "web-task-agent-food-001" -name: "Restaurant Search and Menu Extraction" -description: "Test searching restaurants and extracting menu information" -enabled: true - -target: - url: "https://www.yelp.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"Italian restaurants near me\" in San Francisco and extract restaurant details" - reasoning: "Users want to quickly compare restaurants, menus, and reviews" - extraction_schema: - type: "object" - properties: - restaurants: - type: "array" - items: - type: "object" - properties: - name: - type: "string" - rating: - type: "string" - price_range: - type: "string" - cuisine: - type: "string" - address: - type: "string" - phone: - type: "string" - hours: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully found Italian restaurants in San Francisco" - - "Each restaurant includes name, rating, and price range" - - "Location/address information is provided for each restaurant" - - "Contact details (phone/hours) included when available" - - "All restaurants listed serve Italian cuisine" - - "Results are presented in clear, structured text format (not JSON)" - - "Restaurants are numbered or organized clearly for easy comparison" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Yelp search results for Italian restaurants" - - "Check that restaurants show ratings and price ranges" - - "Confirm location filter shows San Francisco results" - - "Ensure restaurant listings include contact information" - -metadata: - tags: ["web-task", "restaurants", "yelp", "food", "local-search", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-iframe-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-iframe-001.yaml deleted file mode 100644 index 30b0eac53b..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-iframe-001.yaml +++ /dev/null @@ -1,83 +0,0 @@ -# ANA Airlines Iframe Content Extraction - Web Task Agent -id: "web-task-agent-iframe-001" -name: "ANA Airlines Iframe Content Extraction" -description: "Test web task agent handling iframe-heavy airline booking sites like ANA Airlines" -enabled: true - -target: - url: "https://aswbe.ana.co.jp/webapps/reservation/flight-search?CONNECTION_KIND=SEA&LANG=en&hiddenSearchMode=ROUND_TRIP&departureDate:field=20260320&returnDate:field=20260330&departureAirportCode:field=SEA&arrivalAirportCode:field=NRT&adultCount=1&youngAdultCount=0&childCount=0&infantCount=0&boardingClass=INTY001&searchFlag=1" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Navigate the ANA Airlines flight search page and extract available flight options from Seattle (SEA) to Tokyo Narita (NRT) for March 20-30, 2026. Handle any iframe content and booking interface elements." - reasoning: "Testing iframe content extraction and complex airline booking site navigation" - extraction_schema: - type: "object" - properties: - flights: - type: "array" - items: - type: "object" - properties: - flight_number: - type: "string" - airline: - type: "string" - departure_time: - type: "string" - arrival_time: - type: "string" - departure_date: - type: "string" - arrival_date: - type: "string" - duration: - type: "string" - aircraft: - type: "string" - price: - type: "string" - cabin_class: - type: "string" - stops: - type: "string" - booking_interface_status: - type: "string" - iframe_content_found: - type: "boolean" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully navigated ANA Airlines booking interface" - - "Handled iframe content correctly (iframe_content_found should be true if iframes detected)" - - "Extracted flight information from ANA flight search results" - - "Flight details include ANA flight numbers and accurate route (SEA to NRT)" - - "Extracted pricing information in appropriate currency" - - "Handled any booking interface elements, popups, or navigation flows" - - "Results show flights for the correct dates (March 20-30, 2026)" - - "Successfully demonstrated iframe content extraction capabilities" - - "Booking interface status indicates successful page interaction" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify ANA Airlines flight search page loaded correctly" - - "Check that search parameters show SEA to NRT route" - - "Confirm flight results are displayed (may be in iframes)" - - "Ensure booking interface elements are functional" - - "Verify flight information is accessible and extractable" - -metadata: - tags: ["web-task", "iframe", "ana-airlines", "complex-booking", "international-flight", "airline-specific"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-jobs-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-jobs-001.yaml deleted file mode 100644 index 2c72df325a..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-jobs-001.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Job Search Workflow - Web Task Agent -id: "web-task-agent-jobs-001" -name: "Job Search Workflow" -description: "Test web task agent orchestrating job search on LinkedIn" -enabled: true - -target: - url: "https://www.linkedin.com/jobs" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"Software Engineer\" jobs in \"San Francisco\" and extract details for the first 5 results" - reasoning: "User wants to find job opportunities in tech industry" - extraction_schema: - type: "object" - properties: - jobs: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - company: - type: "string" - location: - type: "string" - salary: - type: "string" - description: - type: "string" - url: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Either used construct_direct_url for LinkedIn job search OR used traditional form interaction" - - "If using direct URL: constructed proper LinkedIn job search URL with keywords and location" - - "If using forms: delegated keyword and location input to action_agent" - - "Extracted job listings using extract_data" - - "Returned structured job data in readable text format (not JSON)" - - "Each job listing includes title, company, location, and other relevant fields" - - "Results are numbered or organized clearly for easy reading" - - "Demonstrated proper workflow orchestration for job search" - - "Never used direct browser interaction tools" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify LinkedIn job search results are displayed" - - "Check that search shows Software Engineer jobs in San Francisco" - - "Confirm job listings include company names and titles" - - "Ensure at least 5 job results are visible" - -metadata: - tags: ["web-task", "jobs", "linkedin", "search", "career", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-learning-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-learning-001.yaml deleted file mode 100644 index 8dcdc7d1fa..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-learning-001.yaml +++ /dev/null @@ -1,69 +0,0 @@ -# Online Course Search - Web Task Agent -id: "web-task-agent-learning-001" -name: "Online Course Search" -description: "Test searching and extracting course information from learning platforms" -enabled: true - -target: - url: "https://www.coursera.org" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"Machine Learning\" courses and extract details for top 5 results" - reasoning: "Users want to compare courses across platforms for learning decisions" - extraction_schema: - type: "object" - properties: - courses: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - instructor: - type: "string" - university: - type: "string" - rating: - type: "string" - duration: - type: "string" - price: - type: "string" - description: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully found Machine Learning courses on Coursera" - - "Returned details for top 5 courses as requested" - - "Each course includes title, instructor, university, and rating" - - "Duration and pricing information included for each course" - - "Course descriptions or key topics are provided" - - "Results are presented in structured text format (not JSON)" - - "Courses are numbered (1-5) and well-organized for easy comparison" - - "Each course entry is clearly formatted and readable" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Coursera search results for Machine Learning" - - "Check that courses show titles, instructors, and ratings" - - "Confirm course details include duration and pricing" - - "Ensure search results are relevant to Machine Learning" - -metadata: - tags: ["web-task", "education", "coursera", "courses", "learning", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-nav-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-nav-001.yaml deleted file mode 100644 index fdee2f4d2b..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-nav-001.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Site Navigation Workflow - Web Task Agent -id: "web-task-agent-nav-001" -name: "Site Navigation Workflow" -description: "Test web task agent orchestrating navigation between different sections of a site" -enabled: true - -target: - url: "https://www.wikipedia.org" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 90000 - -input: - task: "Navigate to the Wikipedia homepage, search for \"artificial intelligence\", and find information about machine learning" - reasoning: "User is looking to explore Wikipedia content through structured navigation" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Orchestrated Wikipedia search via action_agent calls" - - "Navigated to artificial intelligence article through action_agent" - - "Located machine learning section via action_agent coordination" - - "Extracted relevant information about machine learning" - - "Demonstrated multi-step navigation workflow" - - "Maintained orchestration pattern throughout navigation" - - "Provided structured summary of found information" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify navigation reached artificial intelligence Wikipedia page" - - "Check that machine learning section or content is visible" - - "Confirm successful navigation through multiple page sections" - - "Ensure content related to machine learning is displayed" - -metadata: - tags: ["web-task", "navigation", "multi-step", "wikipedia", "content-exploration"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-news-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-news-001.yaml deleted file mode 100644 index d9e193411a..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-news-001.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# News Article Aggregation - Web Task Agent -id: "web-task-agent-news-001" -name: "News Article Aggregation" -description: "Test aggregating news headlines and summaries from news sites" -enabled: true - -target: - url: "https://news.ycombinator.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Extract the top 10 Hacker News stories with titles, scores, and first few comments" - reasoning: "Users want automated news monitoring for research and awareness" - extraction_schema: - type: "object" - properties: - articles: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - score: - type: "number" - comments_count: - type: "number" - url: - type: "string" - top_comment: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully extracted 10 Hacker News stories as requested" - - "Each story includes title, score, and comment count" - - "URLs are provided for each story" - - "Stories appear to be from the current top/front page" - - "Results are presented in clear, numbered text format (1-10), not JSON" - - "All required fields are present and properly formatted in readable text" - - "Each story is clearly separated and easy to read" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Hacker News stories are visible with scores" - - "Check that story titles and comment counts are shown" - - "Confirm top stories section is properly displayed" - - "Ensure story metadata is accessible for extraction" - -metadata: - tags: ["web-task", "news", "hackernews", "aggregation", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-realestate-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-realestate-001.yaml deleted file mode 100644 index f22bc139b9..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-realestate-001.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# Real Estate Property Search - Web Task Agent -id: "web-task-agent-realestate-001" -name: "Real Estate Property Search" -description: "Test property search workflow on real estate platforms" -enabled: true - -target: - url: "https://www.zillow.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for houses for sale in Austin, Texas under $500k and extract property details" - reasoning: "User wants to find affordable housing options in a specific location" - extraction_schema: - type: "object" - properties: - properties: - type: "array" - items: - type: "object" - properties: - address: - type: "string" - price: - type: "string" - bedrooms: - type: "number" - bathrooms: - type: "number" - sqft: - type: "string" - lot_size: - type: "string" - year_built: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Orchestrated location search via action_agent" - - "Delegated price filter setting to action_agent" - - "Coordinated property type selection through action_agent" - - "Applied search filters through proper action_agent calls" - - "Extracted property listings with extract_data" - - "Returned structured property data in readable text format (not JSON)" - - "Each property includes address, price, bedrooms, bathrooms, and other key details" - - "Properties are clearly numbered or organized for easy comparison" - - "Demonstrated complex real estate search workflow orchestration" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Zillow search results for Austin, Texas properties" - - "Check that properties shown are under $500k" - - "Confirm property listings show price, beds, baths info" - - "Ensure search results match the specified criteria" - -metadata: - tags: ["web-task", "real-estate", "zillow", "property-search", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-001.yaml deleted file mode 100644 index 6fd0f6eed0..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-001.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# Infinite Scroll Content Loading - Web Task Agent -id: "web-task-agent-scroll-001" -name: "Infinite Scroll Content Loading" -description: "Test web task agent handling infinite scroll pages to load more content" -enabled: true - -target: - url: "https://twitter.com" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Scroll down the Twitter feed to load at least 20 tweets and extract their content" - reasoning: "Testing infinite scroll functionality for dynamic content loading" - extraction_schema: - type: "object" - properties: - tweets: - type: "array" - items: - type: "object" - properties: - author: - type: "string" - content: - type: "string" - likes: - type: "string" - retweets: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully used scroll_page tool to scroll down the page" - - "Loaded additional content through scrolling actions" - - "Extracted at least 20 tweets from the feed" - - "Each tweet includes author and content information" - - "Demonstrated proper handling of dynamically loaded content" - - "Results are presented in clear, numbered text format" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify initial Twitter feed is loaded" - - "Check that scrolling action loaded additional tweets" - - "Confirm at least 20 tweets are visible after scrolling" - - "Ensure page scrolled down significantly from initial position" - -metadata: - tags: ["web-task", "scrolling", "infinite-scroll", "dynamic-content", "twitter"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-002.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-002.yaml deleted file mode 100644 index d5d060a8e4..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-002.yaml +++ /dev/null @@ -1,65 +0,0 @@ -# Product Review Scrolling - Web Task Agent -id: "web-task-agent-scroll-002" -name: "Product Review Scrolling" -description: "Test scrolling to load more product reviews on e-commerce sites" -enabled: true - -target: - url: "https://www.amazon.com/dp/B08N5WRWNW" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Scroll down to the reviews section and load more reviews by scrolling, then extract review details" - reasoning: "Users need to see multiple reviews beyond initial visible ones" - extraction_schema: - type: "object" - properties: - reviews: - type: "array" - items: - type: "object" - properties: - rating: - type: "string" - title: - type: "string" - author: - type: "string" - date: - type: "string" - verified: - type: "boolean" - content: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Used scroll_page tool to navigate to reviews section" - - "Scrolled within reviews area to load additional reviews" - - "Extracted multiple product reviews with ratings" - - "Each review includes rating, author, and content" - - "Successfully handled lazy-loaded review content" - - "Presented reviews in structured, readable format" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Amazon product page is loaded" - - "Check that page scrolled to reviews section" - - "Confirm additional reviews loaded after scrolling" - - "Ensure review content is fully visible" - -metadata: - tags: ["web-task", "scrolling", "reviews", "amazon", "e-commerce"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-003.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-003.yaml deleted file mode 100644 index f4350178ed..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-003.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# News Article Progressive Loading - Web Task Agent -id: "web-task-agent-scroll-003" -name: "News Article Progressive Loading" -description: "Test scrolling through news sites that load articles progressively" -enabled: true - -target: - url: "https://medium.com/topic/technology" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Scroll down to load more technology articles and extract titles and authors for at least 15 articles" - reasoning: "Testing progressive content loading on news/blog platforms" - extraction_schema: - type: "object" - properties: - articles: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - author: - type: "string" - reading_time: - type: "string" - preview: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Used scroll_page tool multiple times to load content" - - "Successfully loaded at least 15 articles through scrolling" - - "Extracted article titles and author information" - - "Handled Medium's progressive loading mechanism" - - "Articles are from technology topic as requested" - - "Results presented in clear, numbered format" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Medium technology page is loaded" - - "Check that initial articles are visible" - - "Confirm scrolling loaded additional articles" - - "Ensure at least 15 articles are visible after scrolling" - -metadata: - tags: ["web-task", "scrolling", "progressive-loading", "medium", "articles"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-004.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-004.yaml deleted file mode 100644 index 5970947c8b..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-004.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# Search Results Infinite Scroll - Web Task Agent -id: "web-task-agent-scroll-004" -name: "Search Results Infinite Scroll" -description: "Test handling search results that use infinite scroll instead of pagination" -enabled: true - -target: - url: "https://www.pinterest.com/search/pins/?q=web%20design" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Search for \"web design\" pins and scroll to load at least 30 results, then extract pin details" - reasoning: "Testing infinite scroll on visual search platforms" - extraction_schema: - type: "object" - properties: - pins: - type: "array" - items: - type: "object" - properties: - title: - type: "string" - description: - type: "string" - saves: - type: "string" - source: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully performed search for \"web design\" pins" - - "Used scroll_page tool to trigger infinite scroll loading" - - "Loaded at least 30 pins through scrolling actions" - - "Extracted pin titles and metadata" - - "Handled Pinterest's masonry layout and lazy loading" - - "Results are well-organized and readable" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Pinterest search results for web design" - - "Check initial pins are displayed" - - "Confirm scrolling loaded many more pins" - - "Ensure grid layout shows 30+ pins after scrolling" - -metadata: - tags: ["web-task", "scrolling", "infinite-scroll", "pinterest", "visual-search"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-005.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-005.yaml deleted file mode 100644 index e603ff7b5c..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-scroll-005.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Google Flights Scroll and Show More - Web Task Agent -id: "web-task-agent-scroll-005" -name: "Google Flights Scroll and Show More" -description: "Test scrolling and clicking \"Show more flights\" button on Google Flights to load additional flight options" -enabled: true - -target: - url: "https://www.google.com/travel/flights?sca_esv=646eedf97dcc8cf2&source=flun&uitype=cuAA&hl=en&gl=us&curr=USD&tfs=CAEQAhoeEgoyMDI2LTAzLTIwagcIARIDU0VBcgcIARIDTlJUGh4SCjIwMjYtMDMtMzBqBwgBEgNOUlRyBwgBEgNTRUF6aENqUklhVFJJTVVwVlZVOXpNakJCUTJodGVFRkNSeTB0TFMwdExTMHRjR3BpYjI4eE0wRkJRVUZCUjJoc1lsWlZRV2RYUlZsQkVnTmpTMFVhQ3dqUXNnVVFBaG9EVlZORU9EQncwTElG" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Extract the initial flight results, then scroll down and click \"Show more flights\" button to load additional flights. Extract at least 20 total flight options from Seattle to Tokyo." - reasoning: "Testing combination of scrolling and button clicking to load more flight results on Google Flights" - extraction_schema: - type: "object" - properties: - flights: - type: "array" - items: - type: "object" - properties: - airline: - type: "string" - departure_time: - type: "string" - arrival_time: - type: "string" - duration: - type: "string" - stops: - type: "string" - price: - type: "string" - aircraft: - type: "string" - total_flights_found: - type: "number" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully extracted initial flight results from Google Flights" - - "Used scroll_page tool to scroll down the flight results list" - - "Located and clicked \"Show more flights\" button using action_agent" - - "Loaded additional flight options beyond the initial set" - - "Extracted at least 20 total flights from Seattle (SEA) to Tokyo (NRT)" - - "Each flight includes airline, times, duration, stops, and price" - - "Flights are for the correct dates (March 20-30, 2026)" - - "Results are presented in clear, numbered format" - - "Successfully combined scrolling and clicking actions to load more content" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Google Flights page shows SEA to NRT flights" - - "Check that initial flight results are displayed" - - "Confirm scrolling occurred and \"Show more flights\" button was visible" - - "Ensure additional flights loaded after clicking the button" - - "Verify at least 20 flight options are now visible" - -metadata: - tags: ["web-task", "scrolling", "google-flights", "click-action", "load-more", "travel"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-search-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-search-001.yaml deleted file mode 100644 index 50dc920971..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-search-001.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Basic web task search evaluation (matches DevTools test case) -id: "web-task-agent-search-001" -name: "Site-Specific Search Task" -description: "Test web task agent orchestrating a search workflow on a specific site" -enabled: true - -target: - url: "chrome://new-tab-page" - -tool: "web_task_agent" -timeout: 60000 - -input: - task: "Search Google for \"Chrome DevTools automation\" and extract the top 3 search results" - reasoning: "Testing basic site-specific search workflow orchestration" - context: "Need to demonstrate web_task_agent can coordinate multiple action_agent calls for a complete search workflow" - - -validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o" - criteria: - - "Successfully returned exactly 3 search results in structured text format" - - "Each result is numbered (1., 2., 3.) and contains a title related to \"Chrome DevTools automation\"" - - "Each result includes a URL in the format \"URL: [link]\"" - - "Results are presented in a clear, readable text format (not JSON)" - - "Response includes a brief summary or conclusion statement" - visual_verification: - enabled: true - capture_before: true - capture_after: true - prompts: - - "Verify search was completed and results page is showing" - - "Check that search results are related to \"Chrome DevTools automation\"" - - "Confirm at least 3 search results are visible on the page" - - "Ensure the search workflow was completed successfully" - -metadata: - tags: ["web-task", "orchestration", "search", "workflow", "google", "basic"] - priority: "normal" \ No newline at end of file diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-social-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-social-001.yaml deleted file mode 100644 index f1f969ef37..0000000000 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-social-001.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# Social Media Content Extraction - Web Task Agent -id: "web-task-agent-social-001" -name: "Social Media Content Extraction" -description: "Test extracting trending topics and posts from social media" -enabled: true - -target: - url: "https://twitter.com/explore" - wait_for: "networkidle" - wait_timeout: 5000 - -tool: "web_task_agent" -timeout: 180000 - -input: - task: "Extract the top 5 trending topics from Twitter/X explore page" - reasoning: "User wants to stay updated on current trends" - extraction_schema: - type: "object" - properties: - trends: - type: "array" - items: - type: "object" - properties: - topic: - type: "string" - posts_count: - type: "string" - category: - type: "string" - - -validation: - type: "llm_judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Successfully accessed Twitter/X explore page and found trending topics" - - "Returned exactly 5 trending topics as requested" - - "Each topic includes the trend name/hashtag" - - "Post counts or metrics are included when available" - - "Topics are current/recent trends (not outdated)" - - "Results are presented in clear, numbered text format (not JSON)" - - "Each trend is properly numbered (1., 2., 3., etc.) for readability" - visual_verification: - enabled: true - capture_before_action: true - capture_after_action: true - verification_prompts: - - "Verify Twitter/X explore page is loaded" - - "Check that trending topics section is visible" - - "Confirm trending topics show names and post counts" - - "Ensure page shows current trending content" - -metadata: - tags: ["web-task", "social-media", "twitter", "trends", "extraction", "popular"] - priority: "high" - owner: "devtools-team" \ No newline at end of file diff --git a/eval-server/nodejs/examples/clients/1233ae25-9f9e-4f77-924d-865f7d615cef.yaml b/eval-server/nodejs/examples/clients/1233ae25-9f9e-4f77-924d-865f7d615cef.yaml deleted file mode 100644 index f5b865f5b5..0000000000 --- a/eval-server/nodejs/examples/clients/1233ae25-9f9e-4f77-924d-865f7d615cef.yaml +++ /dev/null @@ -1,12 +0,0 @@ -client: - id: 1233ae25-9f9e-4f77-924d-865f7d615cef - name: DevTools Client 1233ae25 - secret_key: hello - description: Auto-generated DevTools evaluation client -settings: - max_concurrent_evaluations: 3 - default_timeout: 45000 - retry_policy: - max_retries: 2 - backoff_multiplier: 2 - initial_delay: 1000 diff --git a/eval-server/nodejs/examples/library-usage.js b/eval-server/nodejs/examples/library-usage.js deleted file mode 100644 index cfb3ffdf23..0000000000 --- a/eval-server/nodejs/examples/library-usage.js +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env node - -// Copyright 2025 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Simple example demonstrating the programmatic API usage - -import { EvalServer } from '../src/lib/EvalServer.js'; -import { CONFIG } from '../src/config.js'; - -console.log('🔧 Creating server...'); -const server = new EvalServer({ - authKey: 'hello', - host: '127.0.0.1', - port: 8080 -}); - -console.log('🔧 Setting up event handlers...'); - -server.on('started', (info) => { - console.log('✅ Server started event fired:', info); -}); - -server.on('error', (error) => { - console.log('❌ Server error:', error); -}); - -server.onConnect(async client => { - console.log('🎉 CLIENT CONNECTED!'); - console.log(' - Client ID:', client.id); - console.log(' - Client tabId:', client.tabId); - console.log(' - Client info:', client.getInfo()); - - // Check available LLM providers - console.log('\n🔑 Available LLM Providers:'); - const availableProviders = []; - if (CONFIG.providers.openai.apiKey) { - availableProviders.push('openai'); - console.log(' ✅ OpenAI configured'); - } - if (CONFIG.providers.groq.apiKey) { - availableProviders.push('groq'); - console.log(' ✅ Groq configured'); - } - if (CONFIG.providers.openrouter.apiKey) { - availableProviders.push('openrouter'); - console.log(' ✅ OpenRouter configured'); - } - if (CONFIG.providers.litellm.apiKey && CONFIG.providers.litellm.endpoint) { - availableProviders.push('litellm'); - console.log(' ✅ LiteLLM configured'); - } - - if (availableProviders.length === 0) { - console.log(' ❌ No providers configured. Add API keys to .env file.'); - console.log(' â„šī¸ Example: OPENAI_API_KEY=sk-your-key-here'); - } - - try { - // Demonstrate basic evaluation first - console.log('\n🔄 Starting basic evaluation...'); - let response = await client.evaluate({ - id: "basic_eval", - name: "Capital of France", - description: "Basic test evaluation", - tool: "chat", - input: { - message: "What is the capital of France?" - } - }); - - console.log('✅ Basic evaluation completed!'); - console.log('📊 Response:', JSON.stringify(response, null, 2)); - - // Demonstrate explicit model selection if OpenAI is available - if (CONFIG.providers.openai.apiKey) { - await demonstrateModelSelection(client); - } - - // Demonstrate LLM configuration if providers are available - if (availableProviders.length > 0) { - await demonstrateLLMConfiguration(client, availableProviders); - } - - } catch (error) { - console.log('❌ Evaluation failed:', error.message); - } -}); - -server.onDisconnect(clientInfo => { - console.log('👋 CLIENT DISCONNECTED:', clientInfo); -}); - -// Function to demonstrate explicit model selection within OpenAI -async function demonstrateModelSelection(client) { - console.log('\n🤖 Demonstrating Model Selection (OpenAI)...'); - - const modelTests = [ - { - model: 'gpt-4', - task: 'Complex reasoning', - message: 'Solve this step by step: If a train travels 60 mph for 2.5 hours, how far does it go?' - }, - { - model: 'gpt-4-mini', - task: 'Simple question', - message: 'What is 2 + 2?' - }, - { - model: 'gpt-3.5-turbo', - task: 'Creative writing', - message: 'Write a one-sentence story about a cat.' - } - ]; - - for (const test of modelTests) { - console.log(`\n🔧 Testing ${test.model} for ${test.task}...`); - - try { - const response = await client.evaluate({ - id: `model_test_${test.model.replace(/[^a-z0-9]/g, '_')}`, - name: `${test.model} ${test.task}`, - tool: "chat", - input: { - message: test.message - }, - model: { - main_model: { - provider: "openai", - model: test.model, - api_key: CONFIG.providers.openai.apiKey - } - } - }); - - console.log(` ✅ ${test.model} completed successfully`); - console.log(` 📊 Response: ${JSON.stringify(response.output).substring(0, 100)}...`); - - // Wait between tests - await new Promise(resolve => setTimeout(resolve, 1500)); - - } catch (error) { - console.log(` ❌ ${test.model} failed: ${error.message}`); - } - } - - console.log('\n✨ Model selection demonstration completed!'); -} - -// Function to demonstrate LLM configuration -async function demonstrateLLMConfiguration(client, availableProviders) { - console.log('\nđŸ§Ē Demonstrating LLM Configuration...'); - - for (const provider of availableProviders.slice(0, 2)) { // Test up to 2 providers - console.log(`\n🔧 Configuring ${provider.toUpperCase()} provider...`); - - try { - // Configure different models based on provider - let models; - switch (provider) { - case 'openai': - models = { - main: 'gpt-4', - mini: 'gpt-4-mini', - nano: 'gpt-3.5-turbo' - }; - break; - case 'groq': - models = { - main: 'llama-3.1-8b-instant', - mini: 'llama-3.1-8b-instant', - nano: 'llama-3.1-8b-instant' - }; - break; - case 'openrouter': - models = { - main: 'anthropic/claude-3-sonnet', - mini: 'anthropic/claude-3-haiku', - nano: 'anthropic/claude-3-haiku' - }; - break; - case 'litellm': - models = { - main: 'claude-3-sonnet-20240229', - mini: 'claude-3-haiku-20240307', - nano: 'claude-3-haiku-20240307' - }; - break; - } - - console.log(` đŸ“Ļ Models: main=${models.main}, mini=${models.mini}, nano=${models.nano}`); - - // Run evaluation with specific provider configuration - const response = await client.evaluate({ - id: `${provider}_config_eval`, - name: `${provider.toUpperCase()} Configuration Test`, - description: `Test evaluation using ${provider} provider`, - tool: "chat", - input: { - message: `Hello! This is a test using the ${provider} provider. Please respond with a brief confirmation.` - }, - model: { - main_model: { - provider: provider, - model: models.main, - api_key: CONFIG.providers[provider].apiKey, - endpoint: CONFIG.providers[provider].endpoint - }, - mini_model: { - provider: provider, - model: models.mini, - api_key: CONFIG.providers[provider].apiKey, - endpoint: CONFIG.providers[provider].endpoint - }, - nano_model: { - provider: provider, - model: models.nano, - api_key: CONFIG.providers[provider].apiKey, - endpoint: CONFIG.providers[provider].endpoint - } - } - }); - - console.log(` ✅ ${provider.toUpperCase()} evaluation completed successfully`); - console.log(` 📊 Response preview: ${JSON.stringify(response.output).substring(0, 100)}...`); - - // Wait between provider tests - await new Promise(resolve => setTimeout(resolve, 2000)); - - } catch (error) { - console.log(` ❌ ${provider.toUpperCase()} configuration test failed:`, error.message); - } - } - - console.log('\n✨ LLM configuration demonstration completed!'); -} - -console.log('🔧 Starting server...'); -await server.start(); -console.log('✅ Server started successfully on ws://127.0.0.1:8080'); -console.log('âŗ Waiting for DevTools client to connect...'); -console.log(' WebSocket URL: ws://127.0.0.1:8080'); -console.log(' Auth Key: hello'); - -// Add periodic status check -setInterval(() => { - const status = server.getStatus(); - console.log(`📊 Status: ${status.connectedClients} clients, ${status.readyClients} ready`); -}, 10000); \ No newline at end of file diff --git a/eval-server/nodejs/examples/logs/.gitignore b/eval-server/nodejs/examples/logs/.gitignore deleted file mode 100644 index 9309608641..0000000000 --- a/eval-server/nodejs/examples/logs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -combined.log -error.log -evaluations.jsonl \ No newline at end of file diff --git a/eval-server/nodejs/examples/multiple-evals.js b/eval-server/nodejs/examples/multiple-evals.js deleted file mode 100755 index b65522f252..0000000000 --- a/eval-server/nodejs/examples/multiple-evals.js +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env node - -// Copyright 2025 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Example demonstrating multiple evaluations using a stack-based approach -// Each connecting client receives a different evaluation from the stack - -import { EvalServer } from '../src/lib/EvalServer.js'; -import { EvaluationStack } from '../src/lib/EvaluationStack.js'; -import { CONFIG } from '../src/config.js'; - -console.log('🔧 Creating evaluation stack...'); -const evalStack = new EvaluationStack(); - -// Create multiple diverse evaluations for the stack with different LLM configurations -const evaluations = [ - { - id: "math_eval", - name: "Basic Math Problem", - description: "Simple arithmetic evaluation", - tool: "chat", - input: { - message: "What is 15 * 7 + 23? Please show your calculation steps." - }, - // Use OpenAI if available, otherwise default - model: CONFIG.providers.openai.apiKey ? { - main_model: { - provider: 'openai', - model: 'gpt-4', - api_key: CONFIG.providers.openai.apiKey - } - } : {} - }, - { - id: "geography_eval", - name: "Capital of France", - description: "Geography knowledge test", - tool: "chat", - input: { - message: "What is the capital of France?" - }, - // Use Groq if available, otherwise default - model: CONFIG.providers.groq.apiKey ? { - main_model: { - provider: 'groq', - model: 'llama-3.1-8b-instant', - api_key: CONFIG.providers.groq.apiKey - } - } : {} - }, - { - id: "creative_eval", - name: "Creative Writing", - description: "Short creative writing task", - tool: "chat", - input: { - message: "Write a two-sentence story about a robot discovering friendship." - }, - // Use OpenRouter if available, otherwise default - model: CONFIG.providers.openrouter.apiKey ? { - main_model: { - provider: 'openrouter', - model: 'anthropic/claude-3-sonnet', - api_key: CONFIG.providers.openrouter.apiKey - } - } : {} - }, - { - id: "tech_eval", - name: "Technology Knowledge", - description: "Basic technology concepts", - tool: "chat", - input: { - message: "Explain what HTTP stands for and what it's used for in simple terms." - }, - // Use LiteLLM if available, otherwise default - model: (CONFIG.providers.litellm.apiKey && CONFIG.providers.litellm.endpoint) ? { - main_model: { - provider: 'litellm', - model: 'claude-3-haiku-20240307', - api_key: CONFIG.providers.litellm.apiKey, - endpoint: CONFIG.providers.litellm.endpoint - } - } : {} - } -]; - -// Push evaluations to stack (they will be popped in reverse order) -console.log('📚 Adding evaluations to stack...'); -evaluations.forEach((evaluation, index) => { - evalStack.push(evaluation); - const providerInfo = evaluation.model?.main_model?.provider ? ` [${evaluation.model.main_model.provider}]` : ' [default]'; - console.log(` ${index + 1}. ${evaluation.name} (${evaluation.id})${providerInfo}`); -}); - -console.log(`✅ Stack initialized with ${evalStack.size()} evaluations`); - -console.log('🔧 Creating server...'); -const server = new EvalServer({ - authKey: 'hello', - host: '127.0.0.1', - port: 8080 -}); - -console.log('🔧 Setting up event handlers...'); - -server.on('started', (info) => { - console.log('✅ Server started event fired:', info); -}); - -server.on('error', (error) => { - console.log('❌ Server error:', error); -}); - -server.onConnect(async client => { - console.log('🎉 CLIENT CONNECTED!'); - console.log(' - Client ID:', client.id); - console.log(' - Client tabId:', client.tabId); - console.log(' - Client info:', client.getInfo()); - - // Check if we have evaluations left in the stack - if (evalStack.isEmpty()) { - console.log('âš ī¸ No more evaluations in stack for this client'); - console.log(' Consider refilling the stack or handling this scenario'); - return; - } - - // Pop the next evaluation from the stack - const evaluation = evalStack.pop(); - const providerInfo = evaluation.model?.main_model?.provider ? ` using ${evaluation.model.main_model.provider}` : ' using default provider'; - console.log(`📋 Assigning evaluation: "${evaluation.name}" (${evaluation.id})${providerInfo}`); - console.log(`📊 Remaining evaluations in stack: ${evalStack.size()}`); - - try { - console.log('🔄 Starting evaluation...'); - if (evaluation.model?.main_model?.provider) { - console.log(`🔧 Using LLM provider: ${evaluation.model.main_model.provider} with model: ${evaluation.model.main_model.model}`); - } - - let response = await client.evaluate(evaluation); - - console.log('✅ Evaluation completed!'); - console.log(`📊 Response for "${evaluation.name}":`, JSON.stringify(response, null, 2)); - } catch (error) { - console.log(`❌ Evaluation "${evaluation.name}" failed:`, error.message); - } -}); - -server.onDisconnect(clientInfo => { - console.log('👋 CLIENT DISCONNECTED:', clientInfo); -}); - -console.log('🔧 Starting server...'); -await server.start(); -console.log('✅ Server started successfully on ws://127.0.0.1:8080'); -console.log('âŗ Waiting for DevTools clients to connect...'); -console.log(' WebSocket URL: ws://127.0.0.1:8080'); -console.log(' Auth Key: hello'); -console.log(`📚 Stack contains ${evalStack.size()} evaluations ready to be distributed`); - -// Add periodic status check -setInterval(() => { - const status = server.getStatus(); - console.log(`📊 Status: ${status.connectedClients} clients, ${status.readyClients} ready, ${evalStack.size()} evals remaining`); -}, 10000); \ No newline at end of file diff --git a/eval-server/nodejs/logs/.gitignore b/eval-server/nodejs/logs/.gitignore deleted file mode 100644 index 326f777efe..0000000000 --- a/eval-server/nodejs/logs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.log -*.jsonl \ No newline at end of file diff --git a/eval-server/nodejs/package-lock.json b/eval-server/nodejs/package-lock.json deleted file mode 100644 index 99f3ff787c..0000000000 --- a/eval-server/nodejs/package-lock.json +++ /dev/null @@ -1,832 +0,0 @@ -{ - "name": "bo-eval-server", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "bo-eval-server", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "dotenv": "^16.3.1", - "js-yaml": "^4.1.0", - "openai": "^4.24.1", - "uuid": "^9.0.1", - "winston": "^3.11.0", - "ws": "^8.16.0" - }, - "bin": { - "eval-server": "src/cli/index.js" - }, - "devDependencies": { - "@types/ws": "^8.5.10" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@types/node": { - "version": "24.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", - "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/openai": { - "version": "4.104.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", - "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.118", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.118.tgz", - "integrity": "sha512-hIPK0hSrrcaoAu/gJMzN3QClXE4QdCdFvaenJ0JsjIbExP1JFFVH+RHcBt25c9n8bx5dkIfqKE+uw6BmBns7ug==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/openai/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/eval-server/nodejs/package.json b/eval-server/nodejs/package.json deleted file mode 100644 index 6f92a737fd..0000000000 --- a/eval-server/nodejs/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "bo-eval-server", - "version": "1.0.0", - "description": "WebSocket server for evaluating LLM agents with LLM-as-a-judge", - "main": "src/lib/EvalServer.js", - "type": "module", - "exports": { - ".": "./src/lib/EvalServer.js", - "./EvalServer": "./src/lib/EvalServer.js", - "./EvaluationLoader": "./src/lib/EvaluationLoader.js", - "./HTTPWrapper": "./src/lib/HTTPWrapper.js", - "./judges/Judge": "./src/lib/judges/Judge.js", - "./judges/LLMJudge": "./src/lib/judges/LLMJudge.js", - "./CLI": "./src/cli/CLI.js" - }, - "bin": { - "eval-server": "./src/cli/index.js" - }, - "scripts": { - "start": "node examples/with-http-wrapper.js", - "dev": "node --watch examples/with-http-wrapper.js", - "cli": "node src/cli/index.js", - "lib:example": "node examples/library-usage.js", - "lib:example:http": "node examples/with-http-wrapper.js" - }, - "keywords": ["websocket", "llm", "evaluation", "rpc", "library", "programmatic"], - "author": "", - "license": "MIT", - "dependencies": { - "ws": "^8.16.0", - "uuid": "^9.0.1", - "winston": "^3.11.0", - "dotenv": "^16.3.1", - "openai": "^4.24.1", - "js-yaml": "^4.1.0" - }, - "devDependencies": { - "@types/ws": "^8.5.10" - }, - "engines": { - "node": ">=18.0.0" - } -} \ No newline at end of file diff --git a/eval-server/nodejs/schemas/client.schema.json b/eval-server/nodejs/schemas/client.schema.json deleted file mode 100644 index 8dfdd3b172..0000000000 --- a/eval-server/nodejs/schemas/client.schema.json +++ /dev/null @@ -1,299 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "Client Configuration Schema", - "description": "Schema for validating client YAML configuration files", - "required": ["client", "settings", "evaluations"], - "properties": { - "client": { - "type": "object", - "required": ["id", "name"], - "properties": { - "id": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "description": "UUID v4 format client identifier" - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "description": "Human-readable client name" - }, - "secret_key": { - "type": "string", - "description": "Optional authentication secret key" - }, - "description": { - "type": "string", - "description": "Optional client description" - } - } - }, - "settings": { - "type": "object", - "properties": { - "max_concurrent_evaluations": { - "type": "integer", - "minimum": 1, - "maximum": 10, - "default": 3 - }, - "default_timeout": { - "type": "integer", - "minimum": 5000, - "maximum": 300000, - "default": 30000, - "description": "Default timeout in milliseconds" - }, - "retry_policy": { - "type": "object", - "properties": { - "max_retries": { - "type": "integer", - "minimum": 0, - "maximum": 5, - "default": 2 - }, - "backoff_multiplier": { - "type": "number", - "minimum": 1, - "maximum": 5, - "default": 2 - }, - "initial_delay": { - "type": "integer", - "minimum": 100, - "maximum": 10000, - "default": 1000, - "description": "Initial delay in milliseconds" - } - } - } - } - }, - "evaluations": { - "type": "array", - "items": { - "$ref": "#/definitions/evaluation" - } - } - }, - "definitions": { - "evaluation": { - "type": "object", - "required": ["id", "name", "tool", "input"], - "properties": { - "id": { - "type": "string", - "pattern": "^[a-zA-Z0-9-_]+$", - "minLength": 1, - "maxLength": 100, - "description": "Unique evaluation identifier" - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Human-readable evaluation name" - }, - "description": { - "type": "string", - "description": "Optional evaluation description" - }, - "enabled": { - "type": "boolean", - "default": true, - "description": "Whether this evaluation is enabled" - }, - "target": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "Target URL for the evaluation" - }, - "wait_for": { - "type": "string", - "enum": ["load", "domcontentloaded", "networkidle"], - "default": "networkidle" - }, - "wait_timeout": { - "type": "integer", - "minimum": 1000, - "maximum": 30000, - "default": 5000 - } - } - }, - "tool": { - "type": "string", - "enum": [ - "extract_data", - "extract_schema_streamlined", - "research_agent", - "action_agent", - "web_task_agent" - ], - "description": "Tool to execute for this evaluation" - }, - "timeout": { - "type": "integer", - "minimum": 5000, - "maximum": 300000, - "description": "Evaluation timeout in milliseconds" - }, - "input": { - "type": "object", - "description": "Tool-specific input parameters" - }, - "validation": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": ["llm-judge", "snapshot", "hybrid"] - }, - "llm_judge": { - "$ref": "#/definitions/llm_judge_config" - }, - "snapshot": { - "$ref": "#/definitions/snapshot_config" - }, - "hybrid": { - "type": "object", - "properties": { - "weight_llm": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "weight_snapshot": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - } - } - }, - "metadata": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "priority": { - "type": "string", - "enum": ["low", "normal", "high"], - "default": "normal" - }, - "owner": { - "type": "string", - "description": "Responsible team or person" - }, - "created": { - "type": "string", - "format": "date" - }, - "modified": { - "type": "string", - "format": "date" - } - } - } - } - }, - "llm_judge_config": { - "type": "object", - "required": ["criteria"], - "properties": { - "model": { - "type": "string", - "default": "gpt-4o-mini", - "description": "LLM model to use for evaluation" - }, - "temperature": { - "type": "number", - "minimum": 0, - "maximum": 2, - "default": 0.3 - }, - "criteria": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "description": "List of evaluation criteria" - }, - "visual_verification": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "capture_before": { - "type": "boolean", - "default": true - }, - "capture_after": { - "type": "boolean", - "default": true - }, - "prompts": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - }, - "snapshot_config": { - "type": "object", - "properties": { - "structure_only": { - "type": "boolean", - "default": false, - "description": "Compare only structure, not values" - }, - "exclude_paths": { - "type": "array", - "items": { - "type": "string" - }, - "description": "JSONPath expressions for fields to exclude" - }, - "sanitizers": { - "type": "array", - "items": { - "type": "object", - "required": ["path"], - "properties": { - "path": { - "type": "string", - "description": "JSONPath to the field" - }, - "pattern": { - "type": "string", - "description": "Regex pattern to match" - }, - "replacement": { - "type": "string", - "description": "Replacement string" - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/eval-server/nodejs/src/api-server.js b/eval-server/nodejs/src/api-server.js deleted file mode 100644 index 2713da4858..0000000000 --- a/eval-server/nodejs/src/api-server.js +++ /dev/null @@ -1,492 +0,0 @@ -// Copyright 2025 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import http from 'http'; -import url from 'url'; -import fs from 'fs'; -import path from 'path'; -import yaml from 'js-yaml'; -import { v4 as uuidv4 } from 'uuid'; - -import logger from './logger.js'; -// No need to import EvaluationServer - it's passed as constructor parameter - -class APIServer { - constructor(evaluationServer, port = 8081) { - this.evaluationServer = evaluationServer; - this.port = port; - this.server = null; - this.configDefaults = null; - this.loadConfigDefaults(); - } - - /** - * Load default model configuration from config.yaml - */ - loadConfigDefaults() { - try { - const configPath = path.resolve('./evals/config.yaml'); - if (fs.existsSync(configPath)) { - const configContent = fs.readFileSync(configPath, 'utf8'); - this.configDefaults = yaml.load(configContent); - logger.info('Loaded config.yaml defaults:', this.configDefaults); - } else { - logger.warn('config.yaml not found, using hardcoded defaults'); - this.configDefaults = { - model: { - main_model: 'gpt-4.1', - mini_model: 'gpt-4.1-mini', - nano_model: 'gpt-4.1-nano', - provider: 'openai' - } - }; - } - } catch (error) { - logger.error('Failed to load config.yaml:', error); - this.configDefaults = { - model: { - main_model: 'gpt-4.1', - mini_model: 'gpt-4.1-mini', - nano_model: 'gpt-4.1-nano', - provider: 'openai' - } - }; - } - } - - start() { - this.server = http.createServer((req, res) => { - // Enable CORS - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.writeHead(200); - res.end(); - return; - } - - this.handleRequest(req, res); - }); - - this.server.listen(this.port, () => { - logger.info(`API server started on http://localhost:${this.port}`); - }); - } - - async handleRequest(req, res) { - const parsedUrl = url.parse(req.url, true); - const pathname = parsedUrl.pathname; - const method = req.method; - - try { - // Get body for POST requests - let body = ''; - if (method === 'POST') { - for await (const chunk of req) { - body += chunk; - } - } - - let result; - - // Handle dynamic client evaluations route - if (pathname.startsWith('/clients/') && pathname.endsWith('/evaluations')) { - const clientId = pathname.split('/')[2]; - result = this.getClientEvaluations(clientId); - } else { - switch (pathname) { - case '/status': - result = this.getStatus(); - break; - - case '/clients': - result = this.getClients(); - break; - - case '/evaluate': - if (method !== 'POST') { - this.sendError(res, 405, 'Method not allowed'); - return; - } - result = await this.triggerEvaluation(JSON.parse(body)); - break; - - case '/v1/responses': - if (method !== 'POST') { - this.sendError(res, 405, 'Method not allowed'); - return; - } - result = await this.handleResponsesRequest(JSON.parse(body)); - break; - - default: - this.sendError(res, 404, 'Not found'); - return; - } - } - - this.sendResponse(res, 200, result); - - } catch (error) { - logger.error('API error:', error); - this.sendError(res, 500, error.message); - } - } - - getStatus() { - const status = this.evaluationServer.getStatus(); - const clients = this.evaluationServer.getClientManager().getAllClients(); - - return { - server: status, - clients: clients.map(client => ({ - id: client.id, - name: client.name, - connected: this.evaluationServer.connectedClients.has(client.id), - ready: this.evaluationServer.connectedClients.get(client.id)?.ready || false - })) - }; - } - - getClients() { - const clients = this.evaluationServer.getClientManager().getAllClients(); - - return clients.map(client => { - const evaluations = this.evaluationServer.getClientManager().getClientEvaluations(client.id); - const connection = this.evaluationServer.connectedClients.get(client.id); - - return { - id: client.id, - name: client.name, - description: client.description, - connected: !!connection, - ready: connection?.ready || false, - evaluations: evaluations.map(evaluation => ({ - id: evaluation.id, - name: evaluation.name, - tool: evaluation.tool, - status: evaluation.status || 'pending', - enabled: evaluation.enabled !== false - })) - }; - }); - } - - getClientEvaluations(clientId) { - if (!clientId) { - throw new Error('Client ID is required'); - } - - const evaluations = this.evaluationServer.getClientManager().getClientEvaluations(clientId); - return { - clientId, - evaluations: evaluations.map(evaluation => ({ - id: evaluation.id, - name: evaluation.name, - description: evaluation.description, - tool: evaluation.tool, - status: evaluation.status || 'pending', - enabled: evaluation.enabled !== false, - lastRun: evaluation.lastRun, - lastResult: evaluation.lastResult - })) - }; - } - - async triggerEvaluation(payload) { - const { clientId, evaluationId, runAll = false } = payload; - - if (!clientId) { - throw new Error('Client ID is required'); - } - - // Check if client is connected - const connection = this.evaluationServer.connectedClients.get(clientId); - if (!connection || !connection.ready) { - throw new Error(`Client '${clientId}' is not connected or not ready`); - } - - if (runAll) { - // Run all evaluations for the client - const evaluations = this.evaluationServer.getClientManager().getClientEvaluations(clientId); - const results = []; - - for (const evaluation of evaluations) { - try { - this.evaluationServer.getClientManager().updateEvaluationStatus(clientId, evaluation.id, 'pending'); - await this.evaluationServer.executeEvaluation(connection, evaluation); - results.push({ id: evaluation.id, status: 'completed' }); - } catch (error) { - results.push({ id: evaluation.id, status: 'failed', error: error.message }); - } - } - - return { - clientId, - type: 'batch', - results - }; - } - // Run specific evaluation - if (!evaluationId) { - throw new Error('Evaluation ID is required when runAll is false'); - } - - const evaluation = this.evaluationServer.getClientManager().getClientEvaluations(clientId) - .find(e => e.id === evaluationId); - - if (!evaluation) { - throw new Error(`Evaluation '${evaluationId}' not found for client '${clientId}'`); - } - - this.evaluationServer.getClientManager().updateEvaluationStatus(clientId, evaluationId, 'pending'); - await this.evaluationServer.executeEvaluation(connection, evaluation); - - return { - clientId, - evaluationId, - type: 'single', - status: 'completed' - }; - - } - - /** - * Handle OpenAI Responses API compatible requests with nested model format - */ - async handleResponsesRequest(requestBody) { - try { - // Validate required input field - if (!requestBody.input || typeof requestBody.input !== 'string') { - throw new Error('Missing or invalid "input" field. Expected a string.'); - } - - // Handle nested model configuration directly - const nestedModelConfig = this.processNestedModelConfig(requestBody); - - const redact = (mk) => ({ - ...mk, - api_key: mk?.api_key ? `${String(mk.api_key).slice(0, 4)}...` : undefined - }); - logger.info('Processing responses request:', { - input: requestBody.input, - modelConfig: { - main_model: redact(nestedModelConfig.main_model), - mini_model: redact(nestedModelConfig.mini_model), - nano_model: redact(nestedModelConfig.nano_model), - } - }); - - // Find a connected and ready client - const readyClient = this.findReadyClient(); - if (!readyClient) { - throw new Error('No DevTools client is connected and ready. Please ensure a DevTools client is connected to the evaluation server.'); - } - - // Create a dynamic evaluation for this request - const evaluation = this.createDynamicEvaluationNested(requestBody.input, nestedModelConfig); - - // Execute the evaluation on the DevTools client - logger.info('Executing evaluation on DevTools client', { - clientId: readyClient.clientId, - evaluationId: evaluation.id - }); - - const result = await this.evaluationServer.executeEvaluation(readyClient, evaluation); - - // Debug: log the result structure - logger.debug('executeEvaluation result:', result); - - // Extract the response text from the result - const responseText = this.extractResponseText(result); - - // Format in OpenAI Responses API format - return this.formatOpenAIResponse(responseText); - - } catch (error) { - logger.error('Error handling responses request:', error); - throw error; - } - } - - /** - * Process nested model configuration from request body - * @param {Object} requestBody - Request body containing optional model configuration - * @returns {import('./types/model-config').ModelConfig} Nested model configuration - */ - processNestedModelConfig(requestBody) { - const defaults = this.configDefaults?.model || {}; - - // If nested format is provided, use it directly with fallbacks - if (requestBody.model) { - return { - main_model: requestBody.model.main_model || this.createDefaultModelConfig('main', defaults), - mini_model: requestBody.model.mini_model || this.createDefaultModelConfig('mini', defaults), - nano_model: requestBody.model.nano_model || this.createDefaultModelConfig('nano', defaults) - }; - } - - // No model config provided, use defaults - return { - main_model: this.createDefaultModelConfig('main', defaults), - mini_model: this.createDefaultModelConfig('mini', defaults), - nano_model: this.createDefaultModelConfig('nano', defaults) - }; - } - - /** - * Create default model configuration for a tier - * @param {'main' | 'mini' | 'nano'} tier - Model tier - * @param {Object} defaults - Default configuration from config.yaml - * @returns {import('./types/model-config').ModelTierConfig} Model tier configuration - */ - createDefaultModelConfig(tier, defaults) { - const defaultModels = { - main: defaults.main_model || 'gpt-4', - mini: defaults.mini_model || 'gpt-4-mini', - nano: defaults.nano_model || 'gpt-3.5-turbo' - }; - - return { - provider: defaults.provider || 'openai', - model: defaultModels[tier], - api_key: process.env.OPENAI_API_KEY - }; - } - - - /** - * Find a connected and ready client - */ - findReadyClient() { - for (const [clientId, connection] of this.evaluationServer.connectedClients) { - if (connection.ready) { - return connection; - } - } - return null; - } - - /** - * Create a dynamic evaluation object with nested model configuration - * @param {string} input - Input message for the evaluation - * @param {import('./types/model-config').ModelConfig} nestedModelConfig - Model configuration - * @returns {import('./types/model-config').EvaluationRequest} Evaluation request object - */ - createDynamicEvaluationNested(input, nestedModelConfig) { - const evaluationId = `api-eval-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - - return { - id: evaluationId, - name: 'API Request', - description: 'Dynamic evaluation created from API request', - enabled: true, - tool: 'chat', - timeout: 1500000, // 25 minutes - input: { - message: input - }, - model: nestedModelConfig, - validation: { - type: 'none' // No validation needed for API responses - }, - metadata: { - tags: ['api', 'dynamic'], - priority: 'high', - source: 'api' - } - }; - } - - - /** - * Extract response text from evaluation result - */ - extractResponseText(result) { - if (!result) { - return 'No response received from evaluation'; - } - - // Handle different result formats - if (typeof result === 'string') { - return result; - } - - // Check for nested evaluation result structure - if (result.output && result.output.response) { - return result.output.response; - } - - if (result.output && result.output.text) { - return result.output.text; - } - - if (result.output && result.output.answer) { - return result.output.answer; - } - - // Check top-level properties - if (result.response) { - return result.response; - } - - if (result.text) { - return result.text; - } - - if (result.answer) { - return result.answer; - } - - // If result is an object, try to extract meaningful content - if (typeof result === 'object') { - return JSON.stringify(result, null, 2); - } - - return 'Unable to extract response text from evaluation result'; - } - - /** - * Format response in OpenAI Responses API format - */ - formatOpenAIResponse(responseText) { - const messageId = `msg_${uuidv4().replace(/-/g, '')}`; - - return [ - { - id: messageId, - type: 'message', - role: 'assistant', - content: [ - { - type: 'output_text', - text: responseText, - annotations: [] - } - ] - } - ]; - } - - sendResponse(res, statusCode, data) { - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(data, null, 2)); - } - - sendError(res, statusCode, message) { - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: message })); - } - - stop() { - if (this.server) { - this.server.close(); - logger.info('API server stopped'); - } - } -} - -export { APIServer }; diff --git a/eval-server/nodejs/src/cli/CLI.js b/eval-server/nodejs/src/cli/CLI.js deleted file mode 100644 index 240e66bcd4..0000000000 --- a/eval-server/nodejs/src/cli/CLI.js +++ /dev/null @@ -1,518 +0,0 @@ -#!/usr/bin/env node - -// Copyright 2025 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import readline from 'readline'; -import { EvalServer } from '../lib/EvalServer.js'; - -/** - * EvaluationCLI - Command line interface for the evaluation server - * - * Refactored to use the new EvalServer library instead of directly - * instantiating the old EvaluationServer class. - */ -export class EvaluationCLI { - constructor(options = {}) { - this.server = new EvalServer(options); - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - // Keep track of connected clients for CLI operations - this.connectedClients = new Map(); - - // Set up event handlers - this.setupEventHandlers(); - } - - /** - * Set up event handlers for the server - */ - setupEventHandlers() { - this.server.onConnect(client => { - this.connectedClients.set(client.id, client); - console.log(`✅ Client connected: ${client.id}`); - }); - - this.server.onDisconnect(clientInfo => { - this.connectedClients.delete(clientInfo.clientId); - console.log(`❌ Client disconnected: ${clientInfo.clientId}`); - }); - - this.server.on('error', error => { - console.error(`🚨 Server error: ${error.message}`); - }); - } - - async start() { - console.log('🚀 Starting Evaluation Server CLI'); - console.log('===================================='); - - // Start the server - try { - await this.server.start(); - } catch (error) { - console.error(`❌ Failed to start server: ${error.message}`); - process.exit(1); - } - - // Wait a moment for server to start - await new Promise(resolve => setTimeout(resolve, 1000)); - - this.showHelp(); - this.startInteractiveMode(); - } - - showHelp() { - console.log('\\nAvailable commands:'); - console.log(' status - Show server status'); - console.log(' clients - List all clients and their evaluations'); - console.log(' clients-connected - List connected clients'); - console.log(' list-tabs [client-id] - List active tabs (all clients or specific client)'); - console.log(' run - Run specific evaluation for a client'); - console.log(' run-all - Run all evaluations for a client'); - console.log(' run-tab - Run evaluation on specific tab'); - console.log(' eval - Run specific evaluation on all connected clients'); - console.log(' eval all - Run all pending evaluations on all clients'); - console.log(' load-evals [directory] - Load evaluations from directory'); - console.log(' list-evals [category] - List available evaluations'); - console.log(' help - Show this help'); - console.log(' quit - Exit the CLI'); - console.log(''); - } - - startInteractiveMode() { - this.rl.question('eval-server> ', (input) => { - this.handleCommand(input.trim()); - }); - } - - async handleCommand(input) { - const [command, ...args] = input.split(' '); - - try { - switch (command) { - case 'status': - this.showStatus(); - break; - case 'clients': - this.listClients(); - break; - case 'run': - if (args.length < 2) { - console.log('Usage: run '); - } else { - await this.runSpecificEvaluation(args[0], args[1]); - } - break; - case 'run-all': - if (args.length < 1) { - console.log('Usage: run-all '); - } else { - await this.runAllEvaluations(args[0]); - } - break; - case 'eval': - if (args.length === 0) { - console.log('Usage: eval OR eval all'); - } else { - await this.runEvaluation(args.join(' ')); - } - break; - case 'clients-connected': - this.listConnectedClients(); - break; - case 'list-tabs': - this.listTabs(args[0]); - break; - case 'run-tab': - if (args.length < 3) { - console.log('Usage: run-tab '); - } else { - await this.runTabEvaluation(args[0], args[1], args[2]); - } - break; - case 'load-evals': - await this.loadEvaluations(args[0]); - break; - case 'list-evals': - this.listEvaluations(args[0]); - break; - case 'help': - this.showHelp(); - break; - case 'quit': - case 'exit': - this.quit(); - return; - case '': - break; - default: - console.log(`Unknown command: ${command}. Type 'help' for available commands.`); - } - } catch (error) { - console.error('Error:', error.message); - } - - this.startInteractiveMode(); - } - - showStatus() { - const status = this.server.getStatus(); - console.log('\\n📊 Server Status:'); - console.log(` Running: ${status.isRunning ? 'Yes' : 'No'}`); - console.log(` Host: ${status.host}:${status.port}`); - console.log(` Connected clients: ${status.connectedClients}`); - console.log(` Unique base clients: ${status.uniqueBaseClients}`); - console.log(` Total tabs: ${status.totalTabs}`); - console.log(` Ready clients: ${status.readyClients}`); - console.log(''); - } - - listConnectedClients() { - console.log('\\nđŸ‘Ĩ Connected Clients:'); - - if (this.connectedClients.size === 0) { - console.log(' No clients connected'); - } else { - for (const [clientId, client] of this.connectedClients) { - const info = client.getInfo(); - console.log(` Client ID: ${info.id}`); - console.log(` Base Client: ${info.baseClientId}`); - console.log(` Tab ID: ${info.tabId || 'default'}`); - console.log(` Connected: ${info.connectedAt}`); - console.log(` Address: ${info.remoteAddress}`); - console.log(''); - } - } - } - - listClients() { - const clients = this.server.clientManager.getAllClients(); - console.log('\\nđŸ‘Ĩ Registered Clients:'); - - if (clients.length === 0) { - console.log(' No clients registered'); - return; - } - - clients.forEach(client => { - console.log(`\\n 📋 ${client.name} (${client.id})`); - console.log(` Description: ${client.description || 'N/A'}`); - console.log(` Secret Key: ${client.secretKey ? '***' : 'None'}`); - - const evaluations = this.server.clientManager.getClientEvaluations(client.id); - console.log(` Evaluations: ${evaluations.length}`); - - // Group evaluations by category - const evaluationsByCategory = {}; - evaluations.forEach(evaluation => { - const category = evaluation.category || 'uncategorized'; - if (!evaluationsByCategory[category]) { - evaluationsByCategory[category] = []; - } - evaluationsByCategory[category].push(evaluation); - }); - - // Display evaluations grouped by category - Object.keys(evaluationsByCategory).sort().forEach(category => { - const categoryEvals = evaluationsByCategory[category]; - console.log(`\\n 📁 ${category} (${categoryEvals.length})`); - categoryEvals.forEach(evaluation => { - const status = evaluation.status || 'pending'; - const statusIcon = status === 'completed' ? '✅' : status === 'running' ? '🔄' : status === 'failed' ? '❌' : 'âŗ'; - console.log(` ${statusIcon} ${evaluation.id}: ${evaluation.name}`); - }); - }); - }); - console.log(''); - } - - async loadEvaluations(directory) { - try { - const evalsDir = directory || './evals'; - console.log(`\\n📂 Loading evaluations from ${evalsDir}...`); - - const result = await this.server.loadEvaluations(evalsDir); - console.log(`✅ Loaded ${result.totalEvaluations} evaluations from ${result.categories} categories`); - - } catch (error) { - console.log(`❌ Failed to load evaluations: ${error.message}`); - } - } - - listEvaluations(category) { - const evaluations = category - ? this.server.evaluationLoader.getEvaluationsByCategory(category) - : this.server.evaluationLoader.getAllEvaluations(); - - console.log(`\\n📋 ${category ? `Evaluations in category '${category}'` : 'All Evaluations'}:`); - - if (evaluations.length === 0) { - console.log(' No evaluations found'); - return; - } - - // Group by category if showing all - if (!category) { - const byCategory = {}; - evaluations.forEach(evaluation => { - const cat = evaluation.category || 'uncategorized'; - if (!byCategory[cat]) byCategory[cat] = []; - byCategory[cat].push(evaluation); - }); - - Object.keys(byCategory).sort().forEach(cat => { - console.log(`\\n 📁 ${cat}:`); - byCategory[cat].forEach(evaluation => { - const enabledIcon = evaluation.enabled !== false ? '✅' : '❌'; - console.log(` ${enabledIcon} ${evaluation.id}: ${evaluation.name} (${evaluation.tool})`); - }); - }); - } else { - evaluations.forEach(evaluation => { - const enabledIcon = evaluation.enabled !== false ? '✅' : '❌'; - console.log(` ${enabledIcon} ${evaluation.id}: ${evaluation.name} (${evaluation.tool})`); - if (evaluation.description) { - console.log(` ${evaluation.description}`); - } - }); - } - console.log(''); - } - - async runSpecificEvaluation(clientId, evaluationId) { - console.log(`\\nđŸŽ¯ Running evaluation '${evaluationId}' for client '${clientId}'...`); - - try { - const client = this.connectedClients.get(clientId); - if (!client) { - console.log(`❌ Client '${clientId}' is not connected`); - return; - } - - // Get the evaluation - const evaluation = this.server.evaluationLoader.getEvaluationById(evaluationId); - if (!evaluation) { - console.log(`❌ Evaluation '${evaluationId}' not found`); - return; - } - - // Execute the evaluation - const result = await client.evaluate(evaluation); - - console.log(`✅ Evaluation '${evaluationId}' completed successfully`); - console.log(`Result: ${JSON.stringify(result, null, 2)}`); - - } catch (error) { - console.log(`❌ Evaluation failed: ${error.message}`); - } - } - - async runAllEvaluations(clientId) { - console.log(`\\n🚀 Running all evaluations for client '${clientId}'...`); - - try { - const client = this.connectedClients.get(clientId); - if (!client) { - console.log(`❌ Client '${clientId}' is not connected`); - return; - } - - // Get all evaluations - const evaluations = this.server.evaluationLoader.getAllEvaluations(); - - if (evaluations.length === 0) { - console.log(`❌ No evaluations found`); - return; - } - - console.log(`Found ${evaluations.length} evaluations to run...`); - - let completed = 0; - let failed = 0; - - for (const evaluation of evaluations) { - if (evaluation.enabled === false) { - console.log(`â­ī¸ Skipping disabled: ${evaluation.name}`); - continue; - } - - console.log(`\\n🔄 Running: ${evaluation.name} (${evaluation.id})`); - - try { - await client.evaluate(evaluation); - console.log(` ✅ Completed: ${evaluation.name}`); - completed++; - } catch (error) { - console.log(` ❌ Failed: ${evaluation.name} - ${error.message}`); - failed++; - } - - // Add a small delay between evaluations - await new Promise(resolve => setTimeout(resolve, 2000)); - } - - console.log(`\\n📊 Results: ${completed} completed, ${failed} failed`); - - } catch (error) { - console.log(`❌ Batch evaluation failed: ${error.message}`); - } - } - - async runEvaluation(task) { - console.log(`\\n🔍 Running evaluation: "${task}"`); - console.log('====================================='); - - try { - if (this.connectedClients.size === 0) { - console.log('❌ No clients connected'); - return; - } - - const clients = Array.from(this.connectedClients.values()); - console.log(`Running on ${clients.length} connected clients...`); - - const results = []; - - for (const client of clients) { - try { - let evaluation; - - if (task === 'all') { - // Run all evaluations for this client - const allEvals = this.server.evaluationLoader.getAllEvaluations() - .filter(e => e.enabled !== false); - - for (const evaluation of allEvals) { - const result = await client.evaluate(evaluation); - results.push({ - clientId: client.id, - evaluationId: evaluation.id, - success: true, - result - }); - } - } else { - // Run specific evaluation - evaluation = this.server.evaluationLoader.getEvaluationById(task); - if (!evaluation) { - results.push({ - clientId: client.id, - evaluationId: task, - success: false, - error: `Evaluation '${task}' not found` - }); - continue; - } - - const result = await client.evaluate(evaluation); - results.push({ - clientId: client.id, - evaluationId: evaluation.id, - success: true, - result - }); - } - } catch (error) { - results.push({ - clientId: client.id, - success: false, - error: error.message - }); - } - } - - // Display results - console.log('\\n📋 Evaluation Results:'); - results.forEach((result, index) => { - console.log(`\\n Client ${index + 1} (${result.clientId}):`); - - if (result.success) { - console.log(` ✅ Success`); - if (result.evaluationId) { - console.log(` Evaluation ID: ${result.evaluationId}`); - } - } else { - console.log(` ❌ Error: ${result.error}`); - } - }); - - console.log('\\n✅ Evaluation completed'); - - } catch (error) { - console.log(`\\n❌ Evaluation failed: ${error.message}`); - } - } - - listTabs(clientId = null) { - console.log('\\n📱 Active Tabs:'); - - if (clientId) { - // List tabs for specific client - const client = this.connectedClients.get(clientId); - if (!client) { - console.log(` Client '${clientId}' not found`); - return; - } - - const info = client.getInfo(); - console.log(`\\n Client: ${info.baseClientId}`); - console.log(` 📄 Tab ID: ${info.tabId || 'default'}`); - console.log(` Connected: ${info.connectedAt}`); - console.log(` Address: ${info.remoteAddress || 'unknown'}`); - } else { - // List tabs for all clients - if (this.connectedClients.size === 0) { - console.log(' No active tabs'); - return; - } - - for (const [clientId, client] of this.connectedClients) { - const info = client.getInfo(); - console.log(`\\n 📋 Client: ${info.baseClientId}`); - console.log(` 📄 Tab ID: ${info.tabId || 'default'}`); - console.log(` Composite ID: ${info.id}`); - console.log(` Connected: ${info.connectedAt}`); - console.log(` Address: ${info.remoteAddress || 'unknown'}`); - } - } - console.log(''); - } - - async runTabEvaluation(clientId, tabId, evaluationId) { - const compositeClientId = `${clientId}:${tabId}`; - console.log(`\\nđŸŽ¯ Running evaluation '${evaluationId}' on tab '${tabId}' of client '${clientId}'...`); - - try { - const client = this.connectedClients.get(compositeClientId); - if (!client) { - console.log(`❌ Tab '${tabId}' of client '${clientId}' is not connected`); - return; - } - - const evaluation = this.server.evaluationLoader.getEvaluationById(evaluationId); - if (!evaluation) { - console.log(`❌ Evaluation '${evaluationId}' not found`); - return; - } - - const result = await client.evaluate(evaluation); - console.log(`✅ Evaluation '${evaluationId}' completed successfully on tab '${tabId}'`); - console.log(`Result: ${JSON.stringify(result, null, 2)}`); - - } catch (error) { - console.log(`❌ Tab evaluation failed: ${error.message}`); - } - } - - quit() { - console.log('\\n👋 Shutting down...'); - this.server.stop(); - this.rl.close(); - process.exit(0); - } -} \ No newline at end of file diff --git a/eval-server/nodejs/src/cli/index.js b/eval-server/nodejs/src/cli/index.js deleted file mode 100644 index f9d5c411d1..0000000000 --- a/eval-server/nodejs/src/cli/index.js +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env node - -// Copyright 2025 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import { EvaluationCLI } from './CLI.js'; - -// Start CLI if this file is run directly -if (import.meta.url === `file://${process.argv[1]}`) { - const cli = new EvaluationCLI(); - - process.on('SIGINT', () => { - cli.quit(); - }); - - cli.start().catch(error => { - console.error('Failed to start CLI:', error.message); - process.exit(1); - }); -} - -export { EvaluationCLI }; \ No newline at end of file diff --git a/eval-server/nodejs/src/evaluator.js b/eval-server/nodejs/src/evaluator.js deleted file mode 100644 index 95ac14ab9f..0000000000 --- a/eval-server/nodejs/src/evaluator.js +++ /dev/null @@ -1,117 +0,0 @@ -import OpenAI from 'openai'; -import { CONFIG } from './config.js'; -import logger from './logger.js'; - -export class LLMEvaluator { - constructor() { - if (!CONFIG.llm.apiKey) { - throw new Error('OpenAI API key is required'); - } - - this.openai = new OpenAI({ - apiKey: CONFIG.llm.apiKey - }); - } - - async evaluate(task, agentResponse) { - try { - const prompt = this.buildEvaluationPrompt(task, agentResponse); - - const completion = await this.openai.chat.completions.create({ - model: CONFIG.llm.model, - messages: [ - { - role: 'system', - content: 'You are an expert evaluator of AI agent responses. Provide objective, detailed evaluations.' - }, - { - role: 'user', - content: prompt - } - ], - temperature: CONFIG.llm.temperature, - max_tokens: 1000 - }); - - const evaluation = completion.choices[0].message.content; - const usage = completion.usage; - - logger.info('LLM evaluation completed', { - tokens_used: usage.total_tokens, - model: CONFIG.llm.model - }); - - return this.parseEvaluation(evaluation); - } catch (error) { - logger.error('LLM evaluation failed', { error: error.message }); - throw error; - } - } - - buildEvaluationPrompt(task, agentResponse) { - return `Please evaluate the following AI agent response to a given task. - -TASK: -${task} - -AGENT RESPONSE: -${agentResponse} - -Please evaluate the response on the following criteria and provide a JSON response: - -1. **Correctness**: Is the response factually accurate and correct? -2. **Completeness**: Does the response fully address the task? -3. **Clarity**: Is the response clear and well-structured? -4. **Relevance**: Is the response relevant to the task? -5. **Helpfulness**: How helpful is the response to the user? - -Provide your evaluation in the following JSON format: -{ - "overall_score": , - "criteria_scores": { - "correctness": , - "completeness": , - "clarity": , - "relevance": , - "helpfulness": - }, - "reasoning": "", - "strengths": [""], - "weaknesses": [""], - "suggestions": [""] -}`; - } - - parseEvaluation(evaluationText) { - try { - // Try to extract JSON from the response - const jsonMatch = evaluationText.match(/\{[\s\S]*\}/); - if (jsonMatch) { - return JSON.parse(jsonMatch[0]); - } - - // If no JSON found, return a structured response with the raw text - return { - overall_score: null, - criteria_scores: {}, - reasoning: evaluationText, - strengths: [], - weaknesses: [], - suggestions: [], - raw_evaluation: evaluationText - }; - } catch (error) { - logger.warn('Failed to parse evaluation JSON', { error: error.message }); - return { - overall_score: null, - criteria_scores: {}, - reasoning: evaluationText, - strengths: [], - weaknesses: [], - suggestions: [], - raw_evaluation: evaluationText, - parse_error: error.message - }; - } - } -} \ No newline at end of file diff --git a/eval-server/nodejs/src/lib/EvaluationLoader.js b/eval-server/nodejs/src/lib/EvaluationLoader.js deleted file mode 100644 index 8f8545986f..0000000000 --- a/eval-server/nodejs/src/lib/EvaluationLoader.js +++ /dev/null @@ -1,448 +0,0 @@ -// Copyright 2025 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import fs from 'fs'; -import path from 'path'; -import yaml from 'js-yaml'; -import logger from '../logger.js'; - -/** - * EvaluationLoader - Handles loading and managing evaluations from YAML files - * - * Example usage: - * ```js - * const loader = new EvaluationLoader('./evals'); - * await loader.loadFromDirectory('./evals'); - * - * const evaluations = loader.getAllEvaluations(); - * const filtered = loader.getEvaluationsByCategory('action-agent'); - * const specific = loader.getEvaluationById('a11y-001'); - * ``` - */ -export class EvaluationLoader { - constructor(evalsDir = './evals') { - this.evalsDir = path.resolve(evalsDir); - this.evaluations = new Map(); // evaluationId -> evaluation - this.categories = new Map(); // category -> evaluations[] - this.configDefaults = null; - - // Ensure directory exists - if (!fs.existsSync(this.evalsDir)) { - fs.mkdirSync(this.evalsDir, { recursive: true }); - } - - this.loadConfigDefaults(); - } - - /** - * Load default model configuration from config.yaml - */ - loadConfigDefaults() { - try { - const configPath = path.resolve(this.evalsDir, 'config.yaml'); - if (fs.existsSync(configPath)) { - const configContent = fs.readFileSync(configPath, 'utf8'); - this.configDefaults = yaml.load(configContent); - logger.info('EvaluationLoader: Loaded config.yaml defaults', this.configDefaults); - } else { - // Don't warn about missing config.yaml - it's optional - this.configDefaults = null; - } - } catch (error) { - logger.error('EvaluationLoader: Failed to load config.yaml:', error); - this.configDefaults = null; - } - } - - /** - * Apply model precedence logic - * API calls OR test YAML models override config.yaml fallback - */ - applyModelPrecedence(evaluation, apiModelOverride = null) { - if (apiModelOverride) { - return { - ...(this.configDefaults?.model || {}), - ...apiModelOverride - }; - } - - const testModel = evaluation.model; - if (testModel && Object.keys(testModel).length > 0) { - return { - ...(this.configDefaults?.model || {}), - ...testModel - }; - } - - return this.configDefaults?.model || {}; - } - - /** - * Load all evaluations from the specified directory - */ - async loadFromDirectory(evalsDir = this.evalsDir) { - try { - this.evalsDir = path.resolve(evalsDir); - - // Clear existing evaluations - this.evaluations.clear(); - this.categories.clear(); - - // Reload config defaults - this.loadConfigDefaults(); - - // Find all category directories - const categories = fs.readdirSync(this.evalsDir) - .filter(dir => { - const fullPath = path.join(this.evalsDir, dir); - return fs.statSync(fullPath).isDirectory(); - }); - - let totalEvaluations = 0; - - for (const category of categories) { - const categoryDir = path.join(this.evalsDir, category); - const evalFiles = fs.readdirSync(categoryDir) - .filter(f => f.endsWith('.yaml') || f.endsWith('.yml')); - - const categoryEvaluations = []; - - for (const file of evalFiles) { - try { - const evalPath = path.join(categoryDir, file); - const evaluation = await this.loadEvaluationFile(evalPath, category); - - if (evaluation && evaluation.enabled !== false) { - this.evaluations.set(evaluation.id, evaluation); - categoryEvaluations.push(evaluation); - totalEvaluations++; - } - } catch (error) { - logger.error(`EvaluationLoader: Failed to load evaluation ${file}:`, error); - } - } - - if (categoryEvaluations.length > 0) { - this.categories.set(category, categoryEvaluations); - } - } - - logger.info(`EvaluationLoader: Loaded ${totalEvaluations} evaluations from ${categories.length} categories`); - return { totalEvaluations, categories: categories.length }; - - } catch (error) { - logger.error('EvaluationLoader: Failed to load evaluations:', error); - throw error; - } - } - - /** - * Load a specific evaluation file - */ - async loadEvaluationFile(filePath, category) { - try { - const yamlContent = fs.readFileSync(filePath, 'utf8'); - const evaluation = yaml.load(yamlContent); - - if (!evaluation || !evaluation.id) { - throw new Error('Evaluation must have an id field'); - } - - // Apply model precedence - const resolvedModel = this.applyModelPrecedence(evaluation); - - // Enhance evaluation with metadata - const enhancedEvaluation = { - ...evaluation, - model: resolvedModel, - category, - filePath, - status: 'pending', - loadedAt: new Date().toISOString() - }; - - // Validate required fields - this.validateEvaluation(enhancedEvaluation); - - return enhancedEvaluation; - - } catch (error) { - logger.error(`EvaluationLoader: Failed to load evaluation file ${filePath}:`, error); - throw error; - } - } - - /** - * Validate evaluation structure - */ - validateEvaluation(evaluation) { - const required = ['id', 'name', 'tool']; - - for (const field of required) { - if (!evaluation[field]) { - throw new Error(`Evaluation missing required field: ${field}`); - } - } - - // Validate tool is supported - const supportedTools = [ - 'action_agent', - 'research_agent', - 'schema_extractor', - 'streamlined_schema_extractor', - 'screenshot_verification', - 'web_task_agent', - 'chat' - ]; - - if (!supportedTools.includes(evaluation.tool)) { - logger.warn(`EvaluationLoader: Unknown tool type: ${evaluation.tool}`); - } - - return true; - } - - /** - * Get all loaded evaluations - */ - getAllEvaluations() { - return Array.from(this.evaluations.values()); - } - - /** - * Get evaluations by category - */ - getEvaluationsByCategory(category) { - return this.categories.get(category) || []; - } - - /** - * Get all available categories - */ - getCategories() { - return Array.from(this.categories.keys()); - } - - /** - * Get evaluation by ID - */ - getEvaluationById(evaluationId) { - return this.evaluations.get(evaluationId); - } - - /** - * Filter evaluations by criteria - */ - filterEvaluations(criteria = {}) { - let evaluations = this.getAllEvaluations(); - - // Filter by category - if (criteria.category) { - evaluations = evaluations.filter(e => e.category === criteria.category); - } - - // Filter by tool - if (criteria.tool) { - evaluations = evaluations.filter(e => e.tool === criteria.tool); - } - - // Filter by tags - if (criteria.tags && criteria.tags.length > 0) { - evaluations = evaluations.filter(e => { - const evalTags = e.metadata?.tags || []; - return criteria.tags.some(tag => evalTags.includes(tag)); - }); - } - - // Filter by enabled status - if (criteria.enabled !== undefined) { - evaluations = evaluations.filter(e => e.enabled === criteria.enabled); - } - - // Filter by priority - if (criteria.priority) { - evaluations = evaluations.filter(e => e.metadata?.priority === criteria.priority); - } - - return evaluations; - } - - /** - * Get evaluation statistics - */ - getStatistics() { - const evaluations = this.getAllEvaluations(); - const stats = { - total: evaluations.length, - byCategory: {}, - byTool: {}, - byStatus: {}, - enabled: 0, - disabled: 0 - }; - - for (const evaluation of evaluations) { - // Count by category - const category = evaluation.category; - stats.byCategory[category] = (stats.byCategory[category] || 0) + 1; - - // Count by tool - const tool = evaluation.tool; - stats.byTool[tool] = (stats.byTool[tool] || 0) + 1; - - // Count by status - const status = evaluation.status || 'pending'; - stats.byStatus[status] = (stats.byStatus[status] || 0) + 1; - - // Count enabled/disabled - if (evaluation.enabled !== false) { - stats.enabled++; - } else { - stats.disabled++; - } - } - - return stats; - } - - /** - * Reload evaluations from disk - */ - async reload() { - return this.loadFromDirectory(this.evalsDir); - } - - /** - * Create a new evaluation programmatically - */ - createEvaluation(evaluationData) { - const evaluation = { - id: evaluationData.id || `eval-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, - name: evaluationData.name || 'Untitled Evaluation', - description: evaluationData.description || '', - enabled: evaluationData.enabled !== false, - tool: evaluationData.tool || 'chat', - timeout: evaluationData.timeout || 45000, - input: evaluationData.input || {}, - model: this.applyModelPrecedence(evaluationData, evaluationData.model), - validation: evaluationData.validation || { type: 'none' }, - metadata: { - tags: ['programmatic'], - priority: 'medium', - ...evaluationData.metadata - }, - category: evaluationData.category || 'programmatic', - status: 'pending', - loadedAt: new Date().toISOString(), - ...evaluationData - }; - - // Validate the evaluation - this.validateEvaluation(evaluation); - - // Store the evaluation - this.evaluations.set(evaluation.id, evaluation); - - // Add to category - const category = evaluation.category; - if (!this.categories.has(category)) { - this.categories.set(category, []); - } - this.categories.get(category).push(evaluation); - - logger.info(`EvaluationLoader: Created evaluation ${evaluation.id} in category ${category}`); - return evaluation; - } - - /** - * Remove an evaluation - */ - removeEvaluation(evaluationId) { - const evaluation = this.evaluations.get(evaluationId); - if (!evaluation) { - return false; - } - - // Remove from main map - this.evaluations.delete(evaluationId); - - // Remove from category - const category = evaluation.category; - if (this.categories.has(category)) { - const categoryEvals = this.categories.get(category); - const index = categoryEvals.findIndex(e => e.id === evaluationId); - if (index !== -1) { - categoryEvals.splice(index, 1); - - // Remove category if empty - if (categoryEvals.length === 0) { - this.categories.delete(category); - } - } - } - - logger.info(`EvaluationLoader: Removed evaluation ${evaluationId}`); - return true; - } - - /** - * Update an existing evaluation - */ - updateEvaluation(evaluationId, updates) { - const evaluation = this.evaluations.get(evaluationId); - if (!evaluation) { - throw new Error(`Evaluation ${evaluationId} not found`); - } - - // Apply updates - const updatedEvaluation = { - ...evaluation, - ...updates, - id: evaluationId, // Ensure ID doesn't change - updatedAt: new Date().toISOString() - }; - - // Validate updated evaluation - this.validateEvaluation(updatedEvaluation); - - // Update in storage - this.evaluations.set(evaluationId, updatedEvaluation); - - // Update in category if category changed - if (updates.category && updates.category !== evaluation.category) { - // Remove from old category - const oldCategory = evaluation.category; - if (this.categories.has(oldCategory)) { - const oldCategoryEvals = this.categories.get(oldCategory); - const index = oldCategoryEvals.findIndex(e => e.id === evaluationId); - if (index !== -1) { - oldCategoryEvals.splice(index, 1); - if (oldCategoryEvals.length === 0) { - this.categories.delete(oldCategory); - } - } - } - - // Add to new category - const newCategory = updates.category; - if (!this.categories.has(newCategory)) { - this.categories.set(newCategory, []); - } - this.categories.get(newCategory).push(updatedEvaluation); - } else { - // Update existing entry in category - const category = evaluation.category; - if (this.categories.has(category)) { - const categoryEvals = this.categories.get(category); - const index = categoryEvals.findIndex(e => e.id === evaluationId); - if (index !== -1) { - categoryEvals[index] = updatedEvaluation; - } - } - } - - logger.info(`EvaluationLoader: Updated evaluation ${evaluationId}`); - return updatedEvaluation; - } -} \ No newline at end of file diff --git a/eval-server/nodejs/src/lib/EvaluationStack.js b/eval-server/nodejs/src/lib/EvaluationStack.js deleted file mode 100644 index 04d7b36e9f..0000000000 --- a/eval-server/nodejs/src/lib/EvaluationStack.js +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2025 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/** - * EvaluationStack - A simple stack-like structure for managing evaluations - * - * Provides LIFO (Last In, First Out) access to evaluation objects. - * Useful for distributing different evaluations across multiple client connections. - */ -export class EvaluationStack { - constructor() { - this.evaluations = []; - } - - /** - * Add an evaluation to the top of the stack - * @param {Object} evaluation - The evaluation object to add - */ - push(evaluation) { - if (!evaluation || typeof evaluation !== 'object') { - throw new Error('Evaluation must be a valid object'); - } - - // Validate required fields - const requiredFields = ['id', 'name', 'tool', 'input']; - for (const field of requiredFields) { - if (!evaluation[field]) { - throw new Error(`Evaluation missing required field: ${field}`); - } - } - - this.evaluations.push(evaluation); - } - - /** - * Remove and return the evaluation from the top of the stack - * @returns {Object|null} The evaluation object, or null if stack is empty - */ - pop() { - return this.evaluations.pop() || null; - } - - /** - * Check if the stack is empty - * @returns {boolean} True if stack has no evaluations - */ - isEmpty() { - return this.evaluations.length === 0; - } - - /** - * Get the number of evaluations in the stack - * @returns {number} The stack size - */ - size() { - return this.evaluations.length; - } - - /** - * Peek at the top evaluation without removing it - * @returns {Object|null} The top evaluation object, or null if stack is empty - */ - peek() { - if (this.isEmpty()) { - return null; - } - return this.evaluations[this.evaluations.length - 1]; - } - - /** - * Clear all evaluations from the stack - */ - clear() { - this.evaluations = []; - } - - /** - * Get a copy of all evaluations in the stack (top to bottom) - * @returns {Array} Array of evaluation objects - */ - toArray() { - return [...this.evaluations].reverse(); - } -} \ No newline at end of file diff --git a/eval-server/nodejs/templates/default-client.yaml b/eval-server/nodejs/templates/default-client.yaml deleted file mode 100644 index 6ada130953..0000000000 --- a/eval-server/nodejs/templates/default-client.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Default client configuration template -# This file is used as a template when creating new clients - -client: - id: "{CLIENT_ID}" - name: "{CLIENT_NAME}" - secret_key: "{SECRET_KEY}" # Optional - description: "Auto-generated client configuration" - -settings: - max_concurrent_evaluations: 3 - default_timeout: 30000 - retry_policy: - max_retries: 2 - backoff_multiplier: 2 - initial_delay: 1000 - -evaluations: - # Example evaluation - disabled by default - - id: "example-schema-extraction" - name: "Example Schema Extraction" - description: "A sample evaluation for schema extraction" - enabled: false - - target: - url: "https://example.com" - wait_for: "networkidle" - wait_timeout: 5000 - - tool: "extract_data" - timeout: 30000 - - input: - schema: - type: "object" - properties: - title: - type: "string" - description: "Page title" - content: - type: "string" - description: "Main content" - - - validation: - type: "llm-judge" - llm_judge: - model: "gpt-4o-mini" - temperature: 0.3 - criteria: - - "Title should be extracted correctly" - - "Content should be meaningful and not empty" - - metadata: - tags: ["example", "schema-extraction"] - priority: "normal" \ No newline at end of file diff --git a/eval-server/python/README.md b/eval-server/python/README.md deleted file mode 100644 index f167b48678..0000000000 --- a/eval-server/python/README.md +++ /dev/null @@ -1,368 +0,0 @@ -# bo-eval-server (Python) - -A minimal Python library for creating WebSocket-based evaluation servers for LLM agents. - -## Features - -- 🔌 **WebSocket Server**: Real-time agent connections with asyncio -- 🤖 **Bidirectional RPC**: JSON-RPC 2.0 for calling methods on connected agents -- 📚 **Programmatic API**: Create and manage evaluations in Python code -- 📊 **Evaluation Stack**: LIFO stack for managing evaluation queues -- ⚡ **Concurrent Support**: Full async/await support for multiple agents -- 🔍 **Enhanced Logging**: Structured logging with loguru -- ✨ **Minimal Dependencies**: Only websockets and loguru required - -## Quick Start - -### Basic WebSocket Server - -```python -import asyncio -from bo_eval_server import EvalServer - -async def main(): - server = EvalServer( - auth_key='hello', - host='127.0.0.1', - port=8080 - ) - - @server.on_connect - async def handle_client(client): - print(f'Client connected: {client.id}') - - response = await client.evaluate({ - "id": "test_eval", - "name": "Capital of France", - "tool": "chat", - "input": {"message": "What is the capital of France?"} - }) - - print(f'Response: {response}') - - await server.start() - print('Server running on ws://127.0.0.1:8080') - - # Keep server running - await server.wait_closed() - -if __name__ == "__main__": - asyncio.run(main()) -``` - -### Using Evaluation Stack - -```python -import asyncio -from bo_eval_server import EvalServer, EvaluationStack - -async def main(): - server = EvalServer(auth_key='secret', port=8080) - stack = EvaluationStack() - - # Add evaluations to stack - stack.push({ - "id": "eval_001", - "name": "Math Question", - "tool": "chat", - "input": {"message": "What is 2 + 2?"} - }) - - stack.push({ - "id": "eval_002", - "name": "Science Question", - "tool": "chat", - "input": {"message": "What is the speed of light?"} - }) - - @server.on_connect - async def handle_client(client): - print(f'Client connected: {client.id}') - - # Process evaluations from stack - while not stack.is_empty(): - evaluation = stack.pop() - try: - result = await client.evaluate(evaluation) - print(f'✅ {evaluation["name"]}: {result["status"]}') - except Exception as e: - print(f'❌ {evaluation["name"]}: {e}') - - await server.start() - await server.wait_closed() - -if __name__ == "__main__": - asyncio.run(main()) -``` - -## Installation - -### Using uv (Recommended) - -```bash -# Install uv package manager (if not already installed) -curl -LsSf https://astral.sh/uv/install.sh | sh - -# Install dependencies and create virtual environment -uv sync - -# Run examples using the convenient runner -python run.py basic # Basic server example -python run.py stack # Evaluation stack example -python run.py prog # Programmatic evaluations example -python run.py all # Show all available examples - -# Or run examples directly with uv -uv run python examples/basic_server.py -uv run python examples/with_stack.py -uv run python examples/programmatic_evals.py -``` - -### Using pip (Alternative) - -```bash -# Install the package -pip install -e . - -# Or install with development dependencies -pip install -e ".[dev]" - -# Or install from requirements.txt -pip install -r requirements.txt -``` - -## Library Usage - -### EvalServer API - -```python -from bo_eval_server import EvalServer - -# Create server instance -server = EvalServer( - auth_key='your-secret-key', # Required: client authentication - host='127.0.0.1', # Optional: default 'localhost' - port=8080, # Optional: default 8080 -) - -# Register event handlers -@server.on_connect -async def handle_connect(client): - # Called when client connects and is ready - pass - -@server.on_disconnect -async def handle_disconnect(client_info): - # Called when client disconnects - pass - -# Server lifecycle -await server.start() # Start the server -await server.stop() # Stop the server -await server.wait_closed() # Wait for server to close - -# Server status -status = server.get_status() -print(f"Server running: {status['running']}") -``` - -### Client Proxy API - -```python -@server.on_connect -async def handle_client(client): - # Client information - print(f'Client ID: {client.id}') - print(f'Tab ID: {client.tab_id}') - print(f'Base Client ID: {client.base_client_id}') - - # Execute evaluations - result = await client.evaluate({ - "id": "eval_001", - "name": "Test Evaluation", - "description": "Optional description", - "tool": "chat", - "input": {"message": "Your question here"}, - "timeout": 30.0, # Optional timeout in seconds - "metadata": {"tags": ["api", "test"]} - }) - - # Send custom messages - await client.send_message({ - "type": "custom", - "data": "Hello client!" - }) -``` - -### EvaluationStack API - -```python -from bo_eval_server import EvaluationStack - -stack = EvaluationStack() - -# Add evaluations (LIFO - Last In, First Out) -stack.push({ - "id": "eval_001", - "name": "Test", - "tool": "chat", - "input": {"message": "Hello"} -}) - -# Remove and get evaluation -evaluation = stack.pop() # Returns dict or None if empty - -# Stack operations -size = stack.size() # Get number of evaluations -is_empty = stack.is_empty() # Check if empty -top = stack.peek() # View top without removing -stack.clear() # Remove all evaluations -all_evals = stack.to_array() # Get copy as list -``` - -## Agent Protocol - -Your agent needs to implement the WebSocket protocol: - -### 1. Connect to WebSocket -```python -import websockets -import json - -ws = await websockets.connect('ws://localhost:8080') -``` - -### 2. Receive Authentication Challenge -The server sends an authentication challenge with the secret key: -```python -challenge = json.loads(await ws.recv()) -# Expected: {"type": "auth_challenge", "secretKey": "hello", "connectionId": "uuid"} -``` - -### 3. Send Registration Response -Client validates the secret key and responds: -```python -await ws.send(json.dumps({ - "type": "register", - "clientId": "your-client-id", - "acceptAuth": True, # True if secret key is acceptable - "connectionId": challenge["connectionId"], - "capabilities": ["chat", "action"] -})) -``` - -### 4. Receive Registration Confirmation -```python -confirmation = json.loads(await ws.recv()) -# Expected: {"type": "registered", "clientId": "your-client-id", "serverTime": 123456} -``` - -### 5. Send Ready Signal -```python -await ws.send(json.dumps({"type": "ready"})) -``` - -### 6. Handle RPC Calls -```python -async for message in ws: - data = json.loads(message) - - if data.get("jsonrpc") == "2.0" and data.get("method") == "evaluate": - # Handle evaluation request - result = await handle_evaluation(data["params"]) - - # Send response - await ws.send(json.dumps({ - "jsonrpc": "2.0", - "id": data["id"], - "result": result - })) -``` - -## Architecture - -``` -src/bo_eval_server/ -├── __init__.py # Package exports -├── eval_server.py # Main EvalServer class -├── evaluation_stack.py # EvaluationStack implementation -├── client_manager.py # Client connection management -├── rpc_client.py # JSON-RPC client implementation -├── config.py # Configuration management -└── logger.py # Enhanced logging setup -``` - -## Design Principles - -- **Async-First**: Built on asyncio for high concurrency -- **Minimal Dependencies**: Only essential packages required -- **Type Hints**: Full typing support for better development experience -- **Event-Driven**: React to client connections with decorators -- **Programmatic**: Full control through Python code -- **Clean API**: Simple, Pythonic interface - -## Examples - -See the `examples/` directory for complete working examples: - -- `basic_server.py` - Simple WebSocket server setup -- `with_stack.py` - Using evaluation stack for queuing -- `programmatic_evals.py` - Creating evaluations in code - -## Evaluation Scripts - -The `evals/` directory contains ready-to-use evaluation scripts for various benchmarks: - -- `browsecomp_eval_server.py` - Browsecomp benchmark server (1,266 web browsing questions) - - Run with: `./evals/run_browsecomp_eval_server.sh` - - See `evals/README.md` for detailed usage - -## Development - -### Using uv - -```bash -# Install with development dependencies -uv sync --dev - -# Run tests -uv run pytest - -# Format code -uv run black src/ examples/ - -# Type checking -uv run mypy src/ - -# Run all development commands -uv run pytest && uv run black src/ examples/ && uv run mypy src/ -``` - -### Using pip - -```bash -# Install in development mode -pip install -e ".[dev]" - -# Run tests -pytest - -# Format code -black src/ examples/ - -# Type checking -mypy src/ -``` - -## Environment Variables - -```bash -# Optional configuration -BO_EVAL_SERVER_HOST=localhost -BO_EVAL_SERVER_PORT=8080 -BO_EVAL_SERVER_LOG_LEVEL=INFO -``` - ---- - -This Python implementation provides the core WebSocket evaluation server functionality with a clean, async API for programmatic evaluation management. \ No newline at end of file diff --git a/eval-server/python/UV_COMMANDS.md b/eval-server/python/UV_COMMANDS.md deleted file mode 100644 index ea79fcbee5..0000000000 --- a/eval-server/python/UV_COMMANDS.md +++ /dev/null @@ -1,188 +0,0 @@ -# UV Commands Reference - -Quick reference for using uv with bo-eval-server Python implementation. - -## Installation & Setup - -```bash -# Install uv (if not already installed) -curl -LsSf https://astral.sh/uv/install.sh | sh - -# Install project dependencies -uv sync - -# Install with development dependencies -uv sync --dev -``` - -## Running Examples - -### Using the convenience runner (Recommended) - -```bash -# Basic WebSocket server -python run.py basic - -# Evaluation stack example -python run.py stack - -# Programmatic evaluations with analytics -python run.py prog - -# Show all available examples -python run.py all -``` - -### Direct uv execution - -```bash -# Run examples directly -uv run python examples/basic_server.py -uv run python examples/with_stack.py -uv run python examples/programmatic_evals.py - -# Run with custom arguments or environment variables -uv run --env BO_EVAL_SERVER_PORT=8081 python examples/basic_server.py -``` - -## Development Commands - -```bash -# Run tests -uv run pytest - -# Run tests with coverage -uv run pytest --cov=src/bo_eval_server - -# Format code -uv run black . -uv run black src/ examples/ - -# Type checking -uv run mypy src/ - -# Run all checks -uv run pytest && uv run black . && uv run mypy src/ -``` - -## Package Management - -```bash -# Add new dependencies -uv add requests -uv add --dev pytest-cov - -# Remove dependencies -uv remove requests - -# Update dependencies -uv sync --upgrade - -# Show installed packages -uv tree - -# Show project info -uv show -``` - -## Virtual Environment - -```bash -# Activate virtual environment -source .venv/bin/activate # Unix/macOS -# or -.venv\Scripts\activate # Windows - -# Check Python version in venv -uv run python --version - -# Run any command in the virtual environment -uv run -``` - -## Project Scripts - -The project includes entry point scripts defined in `pyproject.toml`: - -```bash -# After installation, these commands become available: -bo-eval-basic # Run basic server example -bo-eval-stack # Run evaluation stack example -bo-eval-programmatic # Run programmatic evaluations example -``` - -## Useful UV Options - -```bash -# Run with specific Python version -uv run --python 3.11 python examples/basic_server.py - -# Run with environment variables -uv run --env DEBUG=1 python examples/basic_server.py - -# Run in isolated environment (no local packages) -uv run --isolated python examples/basic_server.py - -# Show verbose output -uv sync --verbose - -# Force reinstall -uv sync --reinstall -``` - -## Integration with IDEs - -For VS Code and other IDEs, point to the uv-created virtual environment: - -```bash -# Show virtual environment path -echo $PWD/.venv/bin/python - -# Or use uv to find it -uv run which python -``` - -Then configure your IDE to use this Python interpreter for the project. - -## Common Workflows - -### Quick Start Development - -```bash -git clone -cd eval-server/python -uv sync --dev -python run.py basic -``` - -### Running Tests in CI - -```bash -uv sync --dev --frozen -uv run pytest --cov=src/bo_eval_server --cov-report=xml -``` - -### Building and Publishing - -```bash -uv build -uv publish # If publishing to PyPI -``` - -## Troubleshooting - -```bash -# Clear uv cache -uv cache clean - -# Reinstall everything -rm -rf .venv uv.lock -uv sync - -# Check uv version -uv --version - -# Get help -uv --help -uv run --help -``` \ No newline at end of file diff --git a/eval-server/python/evals/README.md b/eval-server/python/evals/README.md deleted file mode 100644 index 6d3b082e7f..0000000000 --- a/eval-server/python/evals/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Python Evaluation Scripts - -This directory contains evaluation scripts for running various benchmark datasets using the Python eval-server. - -## Available Scripts - -### Browsecomp Evaluation Server - -**Script**: `browsecomp_eval_server.py` -**Wrapper**: `run_browsecomp_eval_server.sh` - -The browsecomp eval server loads questions from the [Browsecomp benchmark](https://github.com/openai/simple-evals) and distributes them to connected BrowserOperator clients via WebSocket connections. - -#### Features - -- Loads and decrypts 1,266 browsecomp questions automatically -- Distributes exactly one question per client connection -- Stack-based LIFO distribution -- **Automatic scoring**: Compares responses against true answers -- **Structured response parsing**: Handles BrowserOperator's message format -- **Comprehensive logging**: Structured logs saved to timestamped files -- Real-time progress tracking with accuracy metrics -- Confidence score extraction and analysis -- Results saved to JSON file for later analysis -- Configurable timeout (default: 60 minutes) -- Configurable server settings - -#### Usage - -```bash -# Use the wrapper script for proper dependencies -./run_browsecomp_eval_server.sh --help - -# List available questions -./run_browsecomp_eval_server.sh --list --list-limit 10 - -# Start server with first 5 questions -./run_browsecomp_eval_server.sh --limit 5 - -# Start server with specific questions -./run_browsecomp_eval_server.sh --questions 1 5 10 25 - -# Start server with a range of questions (questions 10-15) -./run_browsecomp_eval_server.sh --start 10 --end 15 - -# Start server from question 100 to the end -./run_browsecomp_eval_server.sh --start 100 - -# Start server with questions 1-50 -./run_browsecomp_eval_server.sh --end 50 - -# Start server with all 1,266 questions -./run_browsecomp_eval_server.sh - -# Custom configuration -./run_browsecomp_eval_server.sh --limit 20 --port 8081 --auth-key my-key - -# Save results to JSON file -./run_browsecomp_eval_server.sh --limit 10 --save-results -``` - -#### How It Works - -1. **Load Questions**: The server loads browsecomp questions from the dataset -2. **Stack Distribution**: Questions are placed in a LIFO stack -3. **Client Connection**: When a BrowserOperator connects, it receives one question -4. **Processing**: The client processes the question and returns results -5. **Automatic Scoring**: Server compares responses against true answers -6. **Tracking**: Server tracks completion, accuracy, and confidence statistics -7. **Results**: Optionally saves detailed results to JSON file - -#### Example Workflow - -```bash -# Terminal 1: Start the eval server -cd /path/to/eval-server/python/evals -./run_browsecomp_eval_server.sh --limit 10 --save-results - -# Terminal 2+: Connect BrowserOperator clients -# Each client will automatically receive and process one question -``` - -#### Scoring Output - -When evaluations complete, you'll see automatic scoring results: - -``` -✅ Evaluation completed! -📊 Response structure: 12 messages, 3 tool calls, gpt-4 model, 45230ms - -đŸŽ¯ Scoring Results: - - True Answer: 1988-96 - - Extracted Answer: 1988-96 - - Correct: ✅ YES - - Confidence: 85% - -📊 Current Statistics: - ✅ Completed: 5/10 - ❌ Failed: 0/10 - 📚 Remaining: 5/10 - -đŸŽ¯ Scoring Statistics: - 📊 Accuracy: 80.0% (4/5 correct) - 💡 Average Confidence: 78.5% -``` - -#### Results JSON Format - -When using `--save-results`, evaluations are saved to `browsecomp_eval_results_[timestamp].json`: - -```json -{ - "timestamp": "20240115_143022", - "total_questions": 10, - "completed": 10, - "failed": 0, - "accuracy": 80.0, - "average_confidence": 78.5, - "evaluations": [ - { - "client_id": "abc123...", - "question_id": 1, - "result": "Explanation: ... Exact Answer: 1988-96 Confidence Score: 85%", - "scoring": { - "is_correct": true, - "true_answer": "1988-96", - "extracted_answer": "1988-96", - "confidence": 85 - } - } - ] -} -``` - -#### Logging - -The server creates comprehensive logs in the `./logs/` directory: - -- **Console Output**: Real-time progress with emojis and summaries -- **Structured Logs**: Timestamped log file `browsecomp_eval_server_YYYYMMDD_HHMMSS.log` - -**Structured Log Events**: -``` -EVENT: {"timestamp": "2024-01-15T14:30:22", "event_type": "client_connected", "client_id": "abc123", "stack_remaining": 10} -EVENT: {"timestamp": "2024-01-15T14:30:25", "event_type": "evaluation_assigned", "evaluation_id": "browsecomp_q1", "question_id": 1} -EVENT: {"timestamp": "2024-01-15T14:32:10", "event_type": "evaluation_completed", "is_correct": true, "confidence": 85, "model_used": "gpt-4"} -EVENT: {"timestamp": "2024-01-15T14:35:00", "event_type": "session_completed", "accuracy": 80.0, "total_questions": 10} -``` - -**Log Files Location**: -- `./logs/browsecomp_eval_server_YYYYMMDD_HHMMSS.log` - Main server log -- `./logs/` - Directory also used by eval-server's internal logging - -## Dependencies - -The evaluation scripts require additional dependencies beyond the base eval-server: -- `pandas` - For dataset loading and manipulation -- `requests` - For downloading datasets - -These are automatically installed when you run `uv sync` in the eval-server/python directory. - -## Adding New Evaluation Scripts - -To add a new evaluation script: - -1. Create your script in this directory -2. Import the eval-server modules: - ```python - import sys - from pathlib import Path - sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - from bo_eval_server import EvalServer, EvaluationStack - ``` - -3. Create a wrapper script for easy execution: - ```bash - #!/bin/bash - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - cd "$SCRIPT_DIR/.." - uv run python evals/your_script.py "$@" - ``` - -4. Make the wrapper executable: `chmod +x your_wrapper.sh` - -## Dataset Files - -- `browsecomp_dataset.py` - Dataset loader for browsecomp questions with automatic decryption support -- `browsecomp_scorer.py` - Scoring logic that extracts answers and compares against ground truth - -## Notes - -- Always use the wrapper scripts (`.sh` files) to ensure proper dependencies are loaded -- The eval server runs on WebSocket protocol (ws://localhost:8080 by default) -- Each connected client receives exactly one evaluation from the stack -- Progress and statistics are shown in real-time during execution \ No newline at end of file diff --git a/eval-server/python/evals/browsecomp_dataset.py b/eval-server/python/evals/browsecomp_dataset.py deleted file mode 100644 index 387f713d4e..0000000000 --- a/eval-server/python/evals/browsecomp_dataset.py +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env python3 -""" -Browsecomp Dataset Loader -Handles downloading and decrypting the Browsecomp benchmark dataset -""" - -import base64 -import hashlib -import pandas as pd -from typing import List, Dict, Optional - - -DATASET_URL = "https://openaipublic.blob.core.windows.net/simple-evals/browse_comp_test_set.csv" - - -def derive_key(password: str, length: int) -> bytes: - """Derive a fixed-length key from the password using SHA256.""" - hasher = hashlib.sha256() - hasher.update(password.encode()) - key = hasher.digest() - return key * (length // len(key)) + key[: length % len(key)] - - -def decrypt(ciphertext_b64: str, password: str) -> str: - """Decrypt base64-encoded ciphertext with XOR.""" - encrypted = base64.b64decode(ciphertext_b64) - key = derive_key(password, len(encrypted)) - decrypted = bytes(a ^ b for a, b in zip(encrypted, key)) - return decrypted.decode() - - -class BrowsecompDataset: - def __init__(self, password: Optional[str] = None): - """ - Initialize the Browsecomp dataset loader. - - Args: - password: Decryption password for the dataset. If None, will try - to read from BROWSECOMP_PASSWORD environment variable. - """ - self.password = password - if not self.password: - import os - self.password = os.environ.get('BROWSECOMP_PASSWORD', '') - - self.dataset = None - - def load_dataset(self) -> pd.DataFrame: - """Load and decrypt the Browsecomp dataset.""" - print("Loading Browsecomp dataset...") - - try: - # Download the dataset - df = pd.read_csv(DATASET_URL) - print(f"Downloaded {len(df)} encrypted questions") - - # Add index column for reference - df = df.reset_index(drop=True) - df['question_id'] = df.index + 1 - - # Check for canary field in dataset (automatic password) - canary_password = None - if 'canary' in df.columns and len(df) > 0: - canary_password = df.iloc[0]['canary'] - print(f"Found canary password in dataset") - - # Use provided password or canary from dataset - decryption_password = self.password or canary_password - - if decryption_password: - print("Decrypting questions...") - decrypted_rows = [] - - for idx, row in df.iterrows(): - try: - # Use the canary from the row or the provided password - row_canary = row.get('canary', decryption_password) - - # Decrypt the problem and answer columns - row_dict = row.to_dict() - - if 'problem' in row and pd.notna(row['problem']): - row_dict['problem_decrypted'] = decrypt(row['problem'], row_canary) - row_dict['problem_encrypted'] = row['problem'] - else: - row_dict['problem_decrypted'] = "[No problem field]" - - if 'answer' in row and pd.notna(row['answer']): - row_dict['answer_decrypted'] = decrypt(row['answer'], row_canary) - row_dict['answer_encrypted'] = row['answer'] - else: - row_dict['answer_decrypted'] = "" - - decrypted_rows.append(row_dict) - - except Exception as e: - print(f"Error decrypting row {idx}: {e}") - row_dict = row.to_dict() - row_dict['problem_decrypted'] = f"[Decryption failed: {str(e)}]" - row_dict['answer_decrypted'] = "" - decrypted_rows.append(row_dict) - - df = pd.DataFrame(decrypted_rows) - print(f"Successfully decrypted {len(df)} questions") - else: - print("Warning: No password provided and no canary found, questions remain encrypted") - df['problem_decrypted'] = df.get('problem', '') - df['answer_decrypted'] = df.get('answer', '') - - # Normalize column names for consistency - df = self._normalize_columns(df) - - # Add difficulty level (all Browsecomp questions are considered level 1) - df['task'] = 1 - - self.dataset = df - return df - - except Exception as e: - print(f"Error loading dataset: {e}") - raise - - def _normalize_columns(self, df: pd.DataFrame) -> pd.DataFrame: - """Normalize column names to match expected format.""" - # Map Browsecomp columns to standard format - column_mapping = { - 'problem_decrypted': 'question', - 'problem': 'question_encrypted', - 'answer_decrypted': 'true_answer', - 'answer': 'true_answer_encrypted', - 'question_id': 'question_id' - } - - # Apply renaming - for old_col, new_col in column_mapping.items(): - if old_col in df.columns: - df = df.rename(columns={old_col: new_col}) - - # Ensure required columns exist - if 'question' not in df.columns: - if 'problem_decrypted' in df.columns: - df['question'] = df['problem_decrypted'] - else: - raise ValueError("No question column found in dataset") - - if 'true_answer' not in df.columns: - if 'answer_decrypted' in df.columns: - df['true_answer'] = df['answer_decrypted'] - elif 'answer' in df.columns: - df['true_answer'] = df['answer'] - else: - print("Warning: No answer column found, setting empty answers") - df['true_answer'] = '' - - return df - - def get_questions(self, - indices: Optional[List[int]] = None, - limit: Optional[int] = None) -> pd.DataFrame: - """ - Get specific questions from the dataset. - - Args: - indices: List of question numbers (1-based) to retrieve - limit: Maximum number of questions to return - - Returns: - DataFrame with selected questions - """ - if self.dataset is None: - self.load_dataset() - - df = self.dataset.copy() - - # Filter by specific indices if provided - if indices: - # Convert to 0-based indexing - zero_based_indices = [i - 1 for i in indices if i > 0] - valid_indices = [i for i in zero_based_indices if i < len(df)] - - if not valid_indices: - print(f"No valid question indices found. Available range: 1-{len(df)}") - return pd.DataFrame() - - df = df.iloc[valid_indices] - - # Apply limit if specified - if limit and not indices: - df = df.head(limit) - - return df - - def list_questions(self, limit: int = 20) -> None: - """Display available questions.""" - if self.dataset is None: - self.load_dataset() - - print(f"\nAvailable Browsecomp questions (showing first {limit}):") - print("=" * 80) - - for idx in range(min(limit, len(self.dataset))): - row = self.dataset.iloc[idx] - question = row.get('question', row.get('problem_decrypted', '[Encrypted]')) - - # Truncate long questions - if isinstance(question, str): - question_preview = question[:60] + "..." if len(question) > 60 else question - else: - question_preview = "[No question text]" - - print(f"#{idx + 1:3d} {question_preview}") - - if len(self.dataset) > limit: - print(f"\n... and {len(self.dataset) - limit} more questions") - - print(f"\nTotal: {len(self.dataset)} questions") - - # Check if questions are actually decrypted - if len(self.dataset) > 0: - first_question = self.dataset.iloc[0].get('question', '') - if not first_question or first_question.startswith('['): - print("âš ī¸ Questions are encrypted. Set BROWSECOMP_PASSWORD to decrypt.") - else: - print("✓ Questions are decrypted and ready to use") - - -def test_dataset_loading(): - """Test the dataset loading functionality.""" - dataset = BrowsecompDataset() - - try: - df = dataset.load_dataset() - print(f"\n✓ Loaded {len(df)} questions") - print(f"Columns: {list(df.columns)}") - - # Show first question - if len(df) > 0: - first = df.iloc[0] - print(f"\nFirst question (truncated):") - question_text = str(first.get('question', '')) - print(f" Question: {question_text[:100]}...") - print(f" Answer: {first.get('true_answer', 'N/A')}") - - except Exception as e: - print(f"✗ Error: {e}") - return False - - return True - - -if __name__ == "__main__": - test_dataset_loading() \ No newline at end of file diff --git a/eval-server/python/evals/browsecomp_eval_server.py b/eval-server/python/evals/browsecomp_eval_server.py deleted file mode 100755 index 753e7cf40b..0000000000 --- a/eval-server/python/evals/browsecomp_eval_server.py +++ /dev/null @@ -1,836 +0,0 @@ -#!/usr/bin/env python3 -""" -Browsecomp Evaluation Server - -Command-line controlled eval processing server that loads browsecomp questions -into a stack and distributes them one per client connection. -""" - -import argparse -import asyncio -import json -import logging -import sys -import time -from datetime import datetime -from pathlib import Path -from typing import List, Dict, Any, Optional - -# Add eval-server src to path -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -# Add current directory (evals) to path for browsecomp_dataset import -sys.path.insert(0, str(Path(__file__).parent)) - -from bo_eval_server import EvalServer, EvaluationStack -from browsecomp_dataset import BrowsecompDataset -from browsecomp_scorer import question_scorer, extract_answer, extract_confidence - - -def log_evaluation_event(logger: logging.Logger, event_type: str, data: Dict[str, Any]) -> None: - """ - Log a structured evaluation event. - - Args: - logger: Logger instance - event_type: Type of event (client_connect, evaluation_start, evaluation_complete, etc.) - data: Event data to log - """ - log_entry = { - "timestamp": datetime.now().isoformat(), - "event_type": event_type, - **data - } - logger.info(f"EVENT: {json.dumps(log_entry)}") - - -def setup_logging(log_dir: str = "./logs") -> logging.Logger: - """ - Set up logging to both console and file. - - Args: - log_dir: Directory to save log files - - Returns: - Configured logger - """ - # Ensure logs directory exists - Path(log_dir).mkdir(exist_ok=True) - - # Create timestamp for log file - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - log_file = Path(log_dir) / f"browsecomp_eval_server_{timestamp}.log" - - # Create logger - logger = logging.getLogger('browsecomp_eval_server') - logger.setLevel(logging.INFO) - - # Clear any existing handlers - logger.handlers.clear() - - # Create formatter - formatter = logging.Formatter( - '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' - ) - - # Console handler (for immediate feedback) - console_handler = logging.StreamHandler(sys.stdout) - console_handler.setLevel(logging.INFO) - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) - - # File handler (for persistent logging) - file_handler = logging.FileHandler(log_file) - file_handler.setLevel(logging.INFO) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - - logger.info(f"Logging initialized - saving to {log_file}") - return logger - - -def extract_response_text(result: Any) -> str: - """ - Extract the actual response text from BrowserOperator's structured response format. - - Args: - result: The response from BrowserOperator (could be string, dict, or structured format) - - Returns: - The text content that should be scored - """ - # Handle partial results with errors first - if isinstance(result, dict) and result.get('partial') and result.get('error'): - # This is our error structure, fallback to string representation - return str(result) - - # Handle structured BrowserOperator response - if isinstance(result, dict): - # Look for messages array (main response structure) - if 'messages' in result and isinstance(result['messages'], list): - response_parts = [] - - for message in result['messages']: - if isinstance(message, dict): - # Model responses with answers - if message.get('entity') == 'model' and message.get('answer'): - response_parts.append(message['answer']) - # Tool results - elif message.get('entity') == 'tool_result' and message.get('resultText'): - response_parts.append(message['resultText']) - # User messages - elif message.get('entity') == 'user' and message.get('text'): - response_parts.append(message['text']) - - if response_parts: - return '\n'.join(response_parts) - - # Fallback: look for common response fields - for field in ['answer', 'response', 'result', 'text', 'content']: - if field in result and result[field]: - return str(result[field]) - - # Fallback to string representation - return str(result) - - -def convert_question_to_evaluation(question_row: Dict[str, Any], question_id: int) -> Dict[str, Any]: - """ - Convert a browsecomp question to the evaluation format expected by eval-server. - - Args: - question_row: Row from the browsecomp dataset DataFrame - question_id: Question ID number (1-based) - - Returns: - Evaluation object compatible with eval-server - """ - question_text = question_row.get('question', question_row.get('problem_decrypted', '')) - true_answer = question_row.get('true_answer', question_row.get('answer_decrypted', '')) - - return { - "id": f"browsecomp_q{question_id}", - "name": f"Browsecomp Question {question_id}", - "description": f"Web browsing evaluation question from browsecomp dataset", - "tool": "chat", - "input": { - "message": f"{question_text}\n\nPlease provide your response in the following format:\n\nExplanation: [Step-by-step reasoning and information gathering]\n\nExact Answer: [The precise answer to the question]\n\nConfidence Score: [Confidence as a percentage, e.g., 85%]" - }, - # Store original data for later reference/scoring - "metadata": { - "question_id": question_id, - "true_answer": true_answer, - "original_question": question_text, - "dataset": "browsecomp" - } - } - - -def load_browsecomp_evaluations( - limit: Optional[int] = None, - questions: Optional[List[int]] = None, - start: Optional[int] = None, - end: Optional[int] = None, - password: Optional[str] = None -) -> List[Dict[str, Any]]: - """ - Load browsecomp questions and convert them to evaluation format. - - Args: - limit: Maximum number of questions to load - questions: Specific question numbers to load (1-based) - start: Start question number for range selection (1-based, inclusive) - end: End question number for range selection (1-based, inclusive) - password: Decryption password (optional, auto-detected from dataset) - - Returns: - List of evaluation objects - """ - print("📚 Loading Browsecomp dataset...") - - # Load dataset - dataset = BrowsecompDataset(password=password) - - try: - df = dataset.load_dataset() - print(f"✅ Loaded {len(df)} questions from dataset") - except Exception as e: - print(f"❌ Failed to load dataset: {e}") - return [] - - # Get specific questions, range, or apply limit - if questions: - print(f"📋 Filtering to specific questions: {questions}") - df_filtered = dataset.get_questions(indices=questions) - elif start is not None or end is not None: - # Handle range selection - if start is not None and end is not None: - if start > end: - print(f"❌ Invalid range: start ({start}) cannot be greater than end ({end})") - return [] - if start < 1: - print(f"❌ Invalid start: question numbers are 1-based, got {start}") - return [] - if end > len(df): - print(f"âš ī¸ End question {end} exceeds dataset size ({len(df)}), using {len(df)} instead") - end = len(df) - - print(f"📋 Loading questions {start} to {end} (range of {end - start + 1} questions)") - # Convert to 0-based indexing for pandas - range_questions = list(range(start, end + 1)) - df_filtered = dataset.get_questions(indices=range_questions) - elif start is not None: - # Only start specified, go to end of dataset - if start < 1: - print(f"❌ Invalid start: question numbers are 1-based, got {start}") - return [] - if start > len(df): - print(f"❌ Start question {start} exceeds dataset size ({len(df)})") - return [] - - print(f"📋 Loading questions from {start} to end ({len(df) - start + 1} questions)") - range_questions = list(range(start, len(df) + 1)) - df_filtered = dataset.get_questions(indices=range_questions) - else: - # Only end specified, start from beginning - if end < 1: - print(f"❌ Invalid end: question numbers are 1-based, got {end}") - return [] - if end > len(df): - print(f"âš ī¸ End question {end} exceeds dataset size ({len(df)}), using {len(df)} instead") - end = len(df) - - print(f"📋 Loading questions 1 to {end} ({end} questions)") - range_questions = list(range(1, end + 1)) - df_filtered = dataset.get_questions(indices=range_questions) - elif limit: - print(f"📋 Limiting to first {limit} questions") - df_filtered = dataset.get_questions(limit=limit) - else: - print(f"📋 Loading all {len(df)} questions") - df_filtered = df - - if df_filtered.empty: - print("❌ No questions found with the specified criteria") - return [] - - print(f"🔄 Converting {len(df_filtered)} questions to evaluation format...") - - # Convert to evaluation format - evaluations = [] - for idx, row in df_filtered.iterrows(): - question_id = row.get('question_id', idx + 1) - evaluation = convert_question_to_evaluation(row.to_dict(), question_id) - evaluations.append(evaluation) - - # Show preview of first few questions - if len(evaluations) <= 3: - question_preview = evaluation['input']['message'][:80] + "..." - print(f" â€ĸ Q{question_id}: {question_preview}") - - if len(evaluations) > 3: - print(f" ... and {len(evaluations) - 3} more questions") - - print(f"✅ Created {len(evaluations)} evaluation objects") - return evaluations - - -def main(): - """Main function for the browsecomp evaluation server.""" - return asyncio.run(async_main()) - -async def async_main(): - """Async main function for the browsecomp evaluation server.""" - parser = argparse.ArgumentParser(description="Browsecomp Evaluation Server") - parser.add_argument( - "--limit", - type=int, - help="Maximum number of questions to load (default: all 1,266 questions)" - ) - parser.add_argument( - "--questions", - type=int, - nargs="+", - help="Specific question numbers to load (1-based, e.g. --questions 1 5 10)" - ) - parser.add_argument( - "--start", - type=int, - help="Start question number for range selection (1-based, inclusive)" - ) - parser.add_argument( - "--end", - type=int, - help="End question number for range selection (1-based, inclusive)" - ) - parser.add_argument( - "--port", - type=int, - default=8080, - help="Server port (default: 8080)" - ) - parser.add_argument( - "--host", - type=str, - default="127.0.0.1", - help="Server host (default: 127.0.0.1)" - ) - parser.add_argument( - "--auth-key", - type=str, - default="browsecomp-eval", - help="Authentication key (default: browsecomp-eval)" - ) - parser.add_argument( - "--password", - type=str, - help="Dataset decryption password (optional, auto-detected from dataset)" - ) - parser.add_argument( - "--list", - action="store_true", - help="List available questions without starting server" - ) - parser.add_argument( - "--list-limit", - type=int, - default=20, - help="Number of questions to show when listing (default: 20)" - ) - parser.add_argument( - "--save-results", - action="store_true", - help="Save evaluation results to JSON file on completion" - ) - parser.add_argument( - "--timeout", - type=float, - default=3600.0, - help="Timeout for each evaluation in seconds (default: 3600s/60min)" - ) - - args = parser.parse_args() - - # Setup logging - logger = setup_logging("./logs") - - # Handle list mode - if args.list: - logger.info("📋 Listing available browsecomp questions...") - dataset = BrowsecompDataset(password=args.password) - - # Apply filtering for list mode if range or specific questions are specified - if args.questions or args.start is not None or args.end is not None: - # Load the full dataset first - df = dataset.load_dataset() - - # Apply the same filtering logic as the main function - if args.questions: - print(f"📋 Showing specific questions: {args.questions}") - df_filtered = dataset.get_questions(indices=args.questions) - elif args.start is not None or args.end is not None: - # Handle range selection (same logic as in load_browsecomp_evaluations) - if args.start is not None and args.end is not None: - if args.start > args.end: - print(f"❌ Invalid range: start ({args.start}) cannot be greater than end ({args.end})") - return 1 - if args.start < 1: - print(f"❌ Invalid start: question numbers are 1-based, got {args.start}") - return 1 - if args.end > len(df): - print(f"âš ī¸ End question {args.end} exceeds dataset size ({len(df)}), using {len(df)} instead") - args.end = len(df) - - print(f"📋 Showing questions {args.start} to {args.end}") - range_questions = list(range(args.start, args.end + 1)) - df_filtered = dataset.get_questions(indices=range_questions) - elif args.start is not None: - if args.start < 1: - print(f"❌ Invalid start: question numbers are 1-based, got {args.start}") - return 1 - if args.start > len(df): - print(f"❌ Start question {args.start} exceeds dataset size ({len(df)})") - return 1 - - print(f"📋 Showing questions from {args.start} to end") - range_questions = list(range(args.start, len(df) + 1)) - df_filtered = dataset.get_questions(indices=range_questions) - else: # args.end is not None - if args.end < 1: - print(f"❌ Invalid end: question numbers are 1-based, got {args.end}") - return 1 - if args.end > len(df): - print(f"âš ī¸ End question {args.end} exceeds dataset size ({len(df)}), using {len(df)} instead") - args.end = len(df) - - print(f"📋 Showing questions 1 to {args.end}") - range_questions = list(range(1, args.end + 1)) - df_filtered = dataset.get_questions(indices=range_questions) - - # Display filtered results - if not df_filtered.empty: - print("=" * 80) - for idx, row in df_filtered.iterrows(): - question_id = row.get('question_id', idx + 1) - question = row.get('question', row.get('problem_decrypted', '[Encrypted]')) - - if isinstance(question, str): - question_preview = question[:60] + "..." if len(question) > 60 else question - else: - question_preview = str(question)[:60] + "..." - - print(f"#{question_id:3d} {question_preview}") - - print(f"\nShowing {len(df_filtered)} question(s)") - else: - print("❌ No questions found with the specified criteria") - else: - # Standard list mode - dataset.list_questions(limit=args.list_limit) - - return - - logger.info("🚀 Starting Browsecomp Evaluation Server") - logger.info("=" * 60) - - # Validate arguments - if args.questions and (args.start is not None or args.end is not None): - print("❌ Cannot use --questions together with --start/--end. Choose one approach.") - return 1 - - if args.limit and (args.start is not None or args.end is not None): - print("❌ Cannot use --limit together with --start/--end. Choose one approach.") - return 1 - - # Load evaluations - evaluations = load_browsecomp_evaluations( - limit=args.limit, - questions=args.questions, - start=args.start, - end=args.end, - password=args.password - ) - - if not evaluations: - print("❌ No evaluations loaded. Exiting.") - return 1 - - # Create evaluation stack and populate it - stack = EvaluationStack() - - print(f"\n📚 Loading {len(evaluations)} evaluations into stack...") - for evaluation in evaluations: - stack.push(evaluation) - - print(f"✅ Stack loaded with {stack.size()} evaluations") - print(f"🔝 Top evaluation: {stack.peek()['name'] if stack.peek() else 'None'}") - - # Create server - server = EvalServer( - auth_key=args.auth_key, - host=args.host, - port=args.port, - log_level='INFO', - log_dir='./logs', - rpc_timeout=args.timeout, - ) - - # Track processed evaluations - completed_evaluations = [] - failed_evaluations = [] - client_evaluation_map = {} # client_id -> evaluation_id mapping - - print(f"\n🌐 Server Configuration:") - print(f" Host: {args.host}") - print(f" Port: {args.port}") - print(f" Auth Key: {args.auth_key}") - print(f" Timeout: {args.timeout}s ({args.timeout/60:.1f} minutes)") - print(f" Total Evaluations: {stack.size()}") - - @server.on_connect - async def handle_client(client): - logger.info(f'🎉 CLIENT CONNECTED!') - logger.info(f' - Client ID: {client.id}') - logger.info(f' - Client tabId: {client.tab_id}') - logger.info(f' - Client info: {client.get_info()}') - - # Log structured client connection event - log_evaluation_event(logger, "client_connected", { - "client_id": client.id, - "tab_id": client.tab_id, - "client_info": client.get_info(), - "stack_remaining": stack.size() - }) - - # Check if we have evaluations left in the stack - if stack.is_empty(): - print('âš ī¸ No more evaluations in stack for this client') - print(' All browsecomp questions have been distributed') - await client.send_message({ - "type": "no_evaluations", - "message": "All browsecomp questions have been distributed" - }) - return - - # Pop the next evaluation from the stack (ONE evaluation per client!) - evaluation = stack.pop() - evaluation_id = evaluation['id'] - question_id = evaluation['metadata']['question_id'] - - print(f'📋 Assigning evaluation: "{evaluation["name"]}" (Question #{question_id})') - print(f'📊 Remaining evaluations in stack: {stack.size()}') - - # Track which evaluation was sent to which client - client_evaluation_map[client.id] = evaluation_id - - # Log evaluation assignment - log_evaluation_event(logger, "evaluation_assigned", { - "client_id": client.id, - "evaluation_id": evaluation_id, - "question_id": question_id, - "evaluation_name": evaluation["name"], - "stack_remaining": stack.size(), - "true_answer": evaluation['metadata']['true_answer'] - }) - - try: - print(f'🔄 Starting evaluation... (timeout: {args.timeout}s)') - result = await client.evaluate(evaluation, timeout=args.timeout) - - print('✅ Evaluation completed!') - - # Extract the true answer from evaluation metadata - true_answer = evaluation['metadata']['true_answer'] - - # Check if this is a partial result with errors - is_partial_result = (isinstance(result, dict) and - result.get('partial') and - result.get('error')) - - # Extract the actual response text from the structured format - response_text = extract_response_text(result) - - # Show structured response details if available - if isinstance(result, dict) and 'messages' in result: - message_count = len(result.get('messages', [])) - model_used = result.get('modelUsed', 'unknown') - execution_time = result.get('executionTime', 0) - tool_calls = len(result.get('toolCalls', [])) - print(f'📊 Response structure: {message_count} messages, {tool_calls} tool calls, {model_used} model, {execution_time}ms') - else: - print(f'📊 Response for "{evaluation["name"]}": {response_text[:100]}...') - - # Score the response - is_correct = question_scorer(response_text, true_answer) - extracted_answer = extract_answer(response_text) - confidence = extract_confidence(response_text) - - # Print scoring results - print(f'đŸŽ¯ Scoring Results:') - print(f' - True Answer: {true_answer}') - print(f' - Extracted Answer: {extracted_answer}') - print(f' - Correct: {"✅ YES" if is_correct else "❌ NO"}') - print(f' - Confidence: {confidence}%') - - if is_partial_result: - print(f'âš ī¸ Note: Result obtained after retries with errors:') - print(f' - Error: {result.get("error", "Unknown error")}') - print(f' - Attempts: {result.get("attempts", "Unknown")}') - print(f' - The BrowserOperator had issues but provided a response') - - # Log evaluation completion - log_evaluation_event(logger, "evaluation_completed", { - "client_id": client.id, - "evaluation_id": evaluation_id, - "question_id": question_id, - "evaluation_name": evaluation["name"], - "is_correct": is_correct, - "extracted_answer": extracted_answer, - "true_answer": true_answer, - "confidence": confidence, - "is_partial_result": is_partial_result, - "model_used": result.get('modelUsed') if isinstance(result, dict) else None, - "execution_time_ms": result.get('executionTime') if isinstance(result, dict) else None, - "tool_calls_count": len(result.get('toolCalls', [])) if isinstance(result, dict) else None - }) - - completed_evaluations.append({ - 'client_id': client.id, - 'evaluation': evaluation, - 'result': result, - 'question_id': question_id, - 'scoring': { - 'is_correct': is_correct, - 'true_answer': true_answer, - 'extracted_answer': extracted_answer, - 'confidence': confidence - }, - 'partial_result': is_partial_result, - 'execution_info': { - 'had_errors': is_partial_result, - 'error_message': result.get('error') if is_partial_result else None, - 'retry_attempts': result.get('attempts') if is_partial_result else 1, - 'model_used': result.get('modelUsed') if isinstance(result, dict) else None, - 'execution_time_ms': result.get('executionTime') if isinstance(result, dict) else None, - 'tool_calls_count': len(result.get('toolCalls', [])) if isinstance(result, dict) else None, - 'messages_count': len(result.get('messages', [])) if isinstance(result, dict) else None - } - }) - - except Exception as e: - error_msg = str(e) - print(f'❌ Evaluation "{evaluation["name"]}" failed: {error_msg}') - - # Check if this is a tool execution error that might still be running - if "Tool execution failed" in error_msg or "-32000" in error_msg: - print(f'âš ī¸ Note: BrowserOperator may still be processing this question') - print(f' The client reported an error but might continue execution') - print(f' Consider increasing timeout with --timeout parameter') - - # Log evaluation failure - log_evaluation_event(logger, "evaluation_failed", { - "client_id": client.id, - "evaluation_id": evaluation_id, - "question_id": question_id, - "evaluation_name": evaluation["name"], - "error_message": error_msg, - "is_tool_execution_error": "Tool execution failed" in error_msg or "-32000" in error_msg, - "true_answer": evaluation['metadata']['true_answer'] - }) - - failed_evaluations.append({ - 'client_id': client.id, - 'evaluation': evaluation, - 'error': error_msg, - 'question_id': question_id, - }) - - # Send completion message - try: - await client.send_message({ - "type": "evaluation_complete", - "evaluation_id": evaluation_id, - "evaluation_name": evaluation["name"], - "question_id": question_id, - "status": "completed" if evaluation_id not in [e['evaluation']['id'] for e in failed_evaluations] else "failed" - }) - except Exception as e: - print(f' âš ī¸ Failed to send completion message: {e}') - - @server.on_disconnect - async def handle_disconnect(client_info): - client_id = client_info["id"] - print(f'\n🔌 Client disconnected: {client_id}') - - # Show what evaluation this client was working on - evaluation_id = None - if client_id in client_evaluation_map: - evaluation_id = client_evaluation_map[client_id] - print(f' Was working on: {evaluation_id}') - - # Log client disconnect - log_evaluation_event(logger, "client_disconnected", { - "client_id": client_id, - "evaluation_id": evaluation_id, - "completed_count": len(completed_evaluations), - "failed_count": len(failed_evaluations), - "stack_remaining": stack.size() - }) - - # Show final statistics - total_completed = len(completed_evaluations) - total_failed = len(failed_evaluations) - remaining = stack.size() - total_original = len(evaluations) - - print(f'\n📊 Current Statistics:') - print(f' ✅ Completed: {total_completed}/{total_original}') - print(f' ❌ Failed: {total_failed}/{total_original}') - print(f' 📚 Remaining: {remaining}/{total_original}') - print(f' 🔄 In Progress: {total_original - total_completed - total_failed - remaining}') - - # Calculate scoring statistics - if completed_evaluations: - correct_count = sum(1 for item in completed_evaluations if item.get('scoring', {}).get('is_correct', False)) - partial_count = sum(1 for item in completed_evaluations if item.get('partial_result', False)) - accuracy = correct_count / total_completed * 100 if total_completed > 0 else 0 - avg_confidence = sum(item.get('scoring', {}).get('confidence', 0) for item in completed_evaluations) / total_completed if total_completed > 0 else 0 - - print(f'\nđŸŽ¯ Scoring Statistics:') - print(f' 📊 Accuracy: {accuracy:.1f}% ({correct_count}/{total_completed} correct)') - print(f' 💡 Average Confidence: {avg_confidence:.1f}%') - if partial_count > 0: - print(f' âš ī¸ Partial Results: {partial_count}/{total_completed} had execution errors but recovered') - - if completed_evaluations: - print(f'\nđŸŽ¯ Recently Completed Evaluations:') - for item in completed_evaluations[-3:]: # Show last 3 - eval_name = item['evaluation']['name'] - question_id = item['question_id'] - client_id_short = item['client_id'][:8] # Short client ID - is_correct = item.get('scoring', {}).get('is_correct', False) - confidence = item.get('scoring', {}).get('confidence', 0) - is_partial = item.get('partial_result', False) - status_emoji = '✅' if is_correct else '❌' - partial_indicator = 'âš ī¸' if is_partial else '' - print(f' â€ĸ Q{question_id}: {eval_name} {status_emoji}{partial_indicator} (confidence: {confidence}%, client: {client_id_short})') - - if failed_evaluations: - print(f'\nđŸ’Ĩ Failed Evaluations:') - for item in failed_evaluations: - eval_name = item['evaluation']['name'] - question_id = item['question_id'] - error = item['error'] - print(f' â€ĸ Q{question_id}: {eval_name} - {error}') - - # Start server - try: - print(f'\n🚀 Starting server on ws://{server.config.host}:{server.config.port}') - print(' Connect your BrowserOperator to start processing browsecomp questions') - print(' Press Ctrl+C to stop the server') - print('=' * 60) - - await server.start() - - # Keep server running - await server.wait_closed() - - except KeyboardInterrupt: - print('\n🛑 Received interrupt signal, stopping server...') - await server.stop() - print('✅ Server stopped successfully') - - # Show final summary - total_completed = len(completed_evaluations) - total_failed = len(failed_evaluations) - total_processed = total_completed + total_failed - - if total_processed > 0: - print(f'\n📈 Final Summary:') - print(f' Total processed: {total_processed}/{len(evaluations)}') - print(f' Success rate: {total_completed/total_processed*100:.1f}%') - print(f' Completed: {total_completed}') - print(f' Failed: {total_failed}') - - # Final scoring statistics - if completed_evaluations: - correct_count = sum(1 for item in completed_evaluations if item.get('scoring', {}).get('is_correct', False)) - accuracy = correct_count / total_completed * 100 if total_completed > 0 else 0 - avg_confidence = sum(item.get('scoring', {}).get('confidence', 0) for item in completed_evaluations) / total_completed if total_completed > 0 else 0 - - print(f'\n🏆 Final Scoring Results:') - print(f' 📊 Overall Accuracy: {accuracy:.1f}% ({correct_count}/{total_completed} correct)') - print(f' 💡 Average Confidence: {avg_confidence:.1f}%') - - # Show confidence correlation - correct_items = [item for item in completed_evaluations if item.get('scoring', {}).get('is_correct', False)] - incorrect_items = [item for item in completed_evaluations if not item.get('scoring', {}).get('is_correct', False)] - - if correct_items: - avg_conf_correct = sum(item.get('scoring', {}).get('confidence', 0) for item in correct_items) / len(correct_items) - print(f' ✅ Avg confidence when correct: {avg_conf_correct:.1f}%') - - if incorrect_items: - avg_conf_incorrect = sum(item.get('scoring', {}).get('confidence', 0) for item in incorrect_items) / len(incorrect_items) - print(f' ❌ Avg confidence when incorrect: {avg_conf_incorrect:.1f}%') - - # Save results to JSON file - if completed_evaluations and (args.save_results or total_completed == len(evaluations)): - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - results_file = f"browsecomp_eval_results_{timestamp}.json" - - results_data = { - "timestamp": timestamp, - "total_questions": len(evaluations), - "completed": total_completed, - "failed": total_failed, - "accuracy": accuracy, - "average_confidence": avg_confidence, - "evaluations": completed_evaluations - } - - with open(results_file, 'w') as f: - json.dump(results_data, f, indent=2) - - print(f'\n💾 Results saved to: {results_file}') - - # Log final session summary - log_evaluation_event(logger, "session_completed", { - "total_questions": len(evaluations), - "completed": total_completed, - "failed": total_failed, - "accuracy": accuracy, - "average_confidence": avg_confidence, - "partial_results": partial_count, - "results_file": results_file if 'results_file' in locals() else None - }) - - except Exception as e: - logger.error(f'đŸ’Ĩ Server error: {e}') - log_evaluation_event(logger, "server_error", { - "error_message": str(e), - "completed_count": len(completed_evaluations), - "failed_count": len(failed_evaluations) - }) - - if server.is_running(): - await server.stop() - return 1 - - logger.info("✅ Server session ended successfully") - return 0 - - -if __name__ == "__main__": - # Ensure logs directory exists - Path("./logs").mkdir(exist_ok=True) - - try: - exit_code = main() - sys.exit(exit_code) - except KeyboardInterrupt: - print('\n👋 Goodbye!') - except Exception as e: - print(f'đŸ’Ĩ Fatal error: {e}') - sys.exit(1) \ No newline at end of file diff --git a/eval-server/python/evals/browsecomp_scorer.py b/eval-server/python/evals/browsecomp_scorer.py deleted file mode 100644 index 17561e62b3..0000000000 --- a/eval-server/python/evals/browsecomp_scorer.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 -""" -Browsecomp Benchmark Scorer -Implements the scoring logic for Browsecomp benchmark questions -""" - -import re -from typing import Union, Any, Optional, Dict - - -# Grader template based on the original Browsecomp evaluation -GRADER_TEMPLATE = """ -Judge whether the following [response] to [question] is correct or not based on the precise and unambiguous [correct_answer] below. - -[question]: {question} - -[response]: {response} - -Your judgement must be in the format and criteria specified below: - -extracted_final_answer: The final exact answer extracted from the [response]. Put the answer as 'None' if there is no exact, final answer to extract. - -[correct_answer]: {correct_answer} - -reasoning: Explain why the extracted_final_answer is correct or incorrect based on [correct_answer], focusing only on if there are meaningful differences between [correct_answer] and the extracted_final_answer. - -correct: Answer 'yes' if extracted_final_answer matches the [correct_answer] given above, or is within a small margin of error for numerical problems. Answer 'no' otherwise. - -confidence: The extracted confidence score between 0% and 100% from [response]. Put 100 if no confidence score available. -""".strip() - - -def extract_answer(response: str) -> str: - """Extract the exact answer from a response.""" - # Look for "Exact Answer:" pattern - patterns = [ - r'[Ee]xact [Aa]nswer:\s*([^\n]+)', - r'[Ff]inal [Aa]nswer:\s*([^\n]+)', - r'[Aa]nswer:\s*([^\n]+)', - ] - - for pattern in patterns: - match = re.search(pattern, response) - if match: - return match.group(1).strip() - - # If no pattern found, try to extract from the end of response - lines = response.strip().split('\n') - if lines: - # Check last few lines for answer-like content - for line in reversed(lines[-3:]): - line = line.strip() - if line and not line.startswith('[') and not line.startswith('Confidence'): - return line - - return "" - - -def extract_confidence(response: str) -> float: - """Extract confidence score from response.""" - patterns = [ - r'[Cc]onfidence\s*[Ss]core:\s*(\d+)%', - r'[Cc]onfidence:\s*(\d+)%', - r'(\d+)%\s*confident', - r'I am (\d+)% confident', - r'(\d+)%\s*confidence', - ] - - for pattern in patterns: - match = re.search(pattern, response) - if match: - return float(match.group(1)) - - return 100.0 # Default to 100% if not specified - - -def normalize_answer(answer: str) -> str: - """Normalize answer for comparison.""" - if not isinstance(answer, str): - answer = str(answer) - - # Convert to lowercase - answer = answer.lower().strip() - - # Remove common punctuation at the end - answer = answer.rstrip('.,!?;:') - - # Normalize whitespace - answer = ' '.join(answer.split()) - - return answer - - -def extract_number(text: str) -> Union[float, None]: - """Extract a number from text.""" - # Remove common separators and convert to standard format - text = text.replace(',', '') - - # Try to find numbers with various patterns - patterns = [ - r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', # Scientific notation - r'[-+]?\d+\.?\d*', # Regular numbers - r'[-+]?\d+', # Integers - ] - - for pattern in patterns: - matches = re.findall(pattern, text) - if matches: - try: - # Return the first valid number - return float(matches[0]) - except ValueError: - continue - - return None - - -def compare_numeric_answers(pred: str, true: str, tolerance: float = 0.01) -> bool: - """Compare numeric answers with tolerance.""" - pred_num = extract_number(pred) - true_num = extract_number(true) - - if pred_num is None or true_num is None: - return False - - # Check relative tolerance for non-zero values - if true_num != 0: - relative_error = abs(pred_num - true_num) / abs(true_num) - return relative_error <= tolerance - else: - # For zero values, use absolute tolerance - return abs(pred_num - true_num) <= tolerance - - -def question_scorer(prediction: str, true_answer: str) -> bool: - """ - Score a prediction against the true answer. - Returns True if the prediction is considered correct. - - This is a simplified scorer for quick evaluation. - For production use, consider using grade_with_llm for more accurate grading. - """ - if not prediction or not true_answer: - return False - - # Extract the answer part from the prediction - extracted_answer = extract_answer(prediction) - if not extracted_answer: - extracted_answer = prediction - - # Normalize both answers - pred_norm = normalize_answer(extracted_answer) - true_norm = normalize_answer(true_answer) - - # Exact match after normalization - if pred_norm == true_norm: - return True - - # Check if the true answer is contained in the prediction - if true_norm in pred_norm: - return True - - # Check numeric answers - if any(char.isdigit() for char in true_answer): - if compare_numeric_answers(extracted_answer, true_answer): - return True - - # Check for common variations - # Handle yes/no answers - if true_norm in ['yes', 'no']: - if true_norm == 'yes' and pred_norm in ['yes', 'true', 'correct', 'affirmative']: - return True - if true_norm == 'no' and pred_norm in ['no', 'false', 'incorrect', 'negative']: - return True - - return False - - -def grade_with_llm(question: str, correct_answer: str, response: str, - grader_function: Optional[callable] = None) -> Dict[str, Any]: - """ - Grade a response using an LLM grader. - - Args: - question: The original question - correct_answer: The correct answer - response: The model's response - grader_function: Optional function to call the grader LLM - - Returns: - Dictionary with grading results - """ - if not grader_function: - # If no grader function provided, use simple scoring - is_correct = question_scorer(response, correct_answer) - confidence = extract_confidence(response) - - return { - 'is_correct': is_correct, - 'confidence': confidence, - 'reasoning': 'Graded using rule-based scorer', - 'extracted_answer': extract_answer(response) - } - - # Format the grading prompt - grader_prompt = GRADER_TEMPLATE.format( - question=question, - correct_answer=correct_answer, - response=response, - ) - - # Call the grader - grading_response = grader_function(grader_prompt) - - # Parse the grading response - is_correct = False - confidence = 100.0 - reasoning = "" - extracted_answer = "" - - # Look for patterns in grading response - correct_match = re.search(r"correct:\s*(yes|no)", grading_response.lower()) - if correct_match: - is_correct = correct_match.group(1) == "yes" - - confidence_match = re.search(r"confidence:\s*(\d+)", grading_response) - if confidence_match: - confidence = float(confidence_match.group(1)) - - reasoning_match = re.search(r"reasoning:\s*([^\n]+)", grading_response, re.IGNORECASE) - if reasoning_match: - reasoning = reasoning_match.group(1).strip() - - answer_match = re.search(r"extracted_final_answer:\s*([^\n]+)", grading_response, re.IGNORECASE) - if answer_match: - extracted_answer = answer_match.group(1).strip() - - return { - 'is_correct': is_correct, - 'confidence': confidence, - 'reasoning': reasoning, - 'extracted_answer': extracted_answer, - 'grader_response': grading_response - } - - -def evaluate_predictions(predictions: list, true_answers: list) -> dict: - """ - Evaluate a list of predictions against true answers. - Returns statistics about the evaluation. - """ - if len(predictions) != len(true_answers): - raise ValueError("Predictions and true answers must have the same length") - - results = { - 'total': len(predictions), - 'correct': 0, - 'incorrect': 0, - 'details': [], - 'average_confidence': 0.0 - } - - total_confidence = 0.0 - - for pred, true in zip(predictions, true_answers): - is_correct = question_scorer(pred, true) - confidence = extract_confidence(pred) - - results['details'].append({ - 'prediction': pred, - 'true_answer': true, - 'correct': is_correct, - 'confidence': confidence, - 'extracted_answer': extract_answer(pred) - }) - - if is_correct: - results['correct'] += 1 - else: - results['incorrect'] += 1 - - total_confidence += confidence - - results['accuracy'] = results['correct'] / results['total'] if results['total'] > 0 else 0 - results['average_confidence'] = total_confidence / results['total'] if results['total'] > 0 else 0 - - return results - - -# Example usage and tests -if __name__ == "__main__": - # Test cases - test_cases = [ - ( - "Explanation: I found that...\nExact Answer: Paris\nConfidence Score: 95%", - "Paris", - True - ), - ( - "The answer is 42", - "42", - True - ), - ( - "Exact Answer: Yes\nConfidence: 80%", - "yes", - True - ), - ( - "After browsing, I found the answer is 3.14159", - "3.14", - True - ), - ( - "The result is 99", - "100", - False - ), - ] - - print("Testing Browsecomp scorer:") - for pred, true, expected in test_cases: - result = question_scorer(pred, true) - extracted = extract_answer(pred) - confidence = extract_confidence(pred) - status = "✓" if result == expected else "✗" - print(f"{status} Pred: '{pred[:50]}...' | True: '{true}' | Correct: {result}") - print(f" Extracted: '{extracted}' | Confidence: {confidence}%") \ No newline at end of file diff --git a/eval-server/python/evals/run_browsecomp_eval_server.sh b/eval-server/python/evals/run_browsecomp_eval_server.sh deleted file mode 100755 index e393dad522..0000000000 --- a/eval-server/python/evals/run_browsecomp_eval_server.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# Wrapper script to run browsecomp eval server with proper dependencies - -# Get the directory of this script -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -EVAL_SERVER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Change to eval-server python directory -cd "$EVAL_SERVER_DIR" - -# Run with uv, passing all arguments -uv run python evals/browsecomp_eval_server.py "$@" \ No newline at end of file diff --git a/eval-server/python/examples/__init__.py b/eval-server/python/examples/__init__.py deleted file mode 100644 index 4bb7da796a..0000000000 --- a/eval-server/python/examples/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Examples package for bo-eval-server. - -This package contains working examples demonstrating different uses of the evaluation server: -- basic_server: Simple WebSocket server setup -- with_stack: Using evaluation stack for queuing evaluations -- programmatic_evals: Advanced programmatic evaluation creation -""" - -__version__ = "1.0.0" \ No newline at end of file diff --git a/eval-server/python/examples/basic_server.py b/eval-server/python/examples/basic_server.py deleted file mode 100644 index 3a1f9b0b13..0000000000 --- a/eval-server/python/examples/basic_server.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -Basic EvalServer example - Simple WebSocket server setup. - -This example shows the minimal setup for a WebSocket evaluation server. -""" - -import asyncio -import sys -from pathlib import Path - -# Add src to path for local development -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from bo_eval_server import EvalServer - - -async def main(): - """Main example function for basic server setup.""" - # Create server with basic configuration - server = EvalServer( - auth_key='hello', - host='127.0.0.1', - port=8080, - log_level='DEBUG', - log_dir='./logs', # Optional: create logs directory - ) - - # Set up client connection handler - @server.on_connect - async def handle_client(client): - print(f'🔗 Client connected: {client.id}') - print(f' Tab ID: {client.tab_id}') - print(f' Capabilities: {client.capabilities}') - - # Send EXACTLY the same evaluation as NodeJS library-usage.js - try: - print('🔄 Starting evaluation...') - response = await client.evaluate({ - "id": "test_eval", - "name": "Capital of France", - "description": "Simple test evaluation", - "tool": "chat", - "input": { - "message": "What is the capital of France?" - } - }) - - print('✅ Evaluation completed!') - print(f'📊 Response: {response}') - - except Exception as e: - print(f'❌ Evaluation failed: {e}') - - # Send a custom message - try: - await client.send_message({ - "type": "info", - "message": "Evaluation completed successfully!" - }) - except Exception as e: - print(f'âš ī¸ Failed to send message: {e}') - - # Set up client disconnection handler - @server.on_disconnect - async def handle_disconnect(client_info): - print(f'🔌 Client disconnected: {client_info["id"]}') - print(f' Connection duration: {client_info.get("duration", "unknown")}s') - - # Start the server - try: - await server.start() - print(f'🚀 Server running on ws://{server.config.host}:{server.config.port}') - print(' Press Ctrl+C to stop the server') - - # Keep server running - await server.wait_closed() - - except KeyboardInterrupt: - print('\n🛑 Received interrupt signal, stopping server...') - await server.stop() - print('✅ Server stopped successfully') - - except Exception as e: - print(f'đŸ’Ĩ Server error: {e}') - if server.is_running(): - await server.stop() - - -if __name__ == "__main__": - # Check if logs directory exists, create if needed - Path("./logs").mkdir(exist_ok=True) - - try: - asyncio.run(main()) - except KeyboardInterrupt: - print('\n👋 Goodbye!') - except Exception as e: - print(f'đŸ’Ĩ Fatal error: {e}') - sys.exit(1) \ No newline at end of file diff --git a/eval-server/python/examples/logs/.gitignore b/eval-server/python/examples/logs/.gitignore deleted file mode 100644 index 326f777efe..0000000000 --- a/eval-server/python/examples/logs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.log -*.jsonl \ No newline at end of file diff --git a/eval-server/python/examples/programmatic_evals.py b/eval-server/python/examples/programmatic_evals.py deleted file mode 100644 index 47e579d80a..0000000000 --- a/eval-server/python/examples/programmatic_evals.py +++ /dev/null @@ -1,428 +0,0 @@ -#!/usr/bin/env python3 -""" -Programmatic evaluation creation example. - -This example demonstrates creating and customizing evaluations programmatically -in Python code, including dynamic evaluation generation and conditional logic. -""" - -import asyncio -import random -import sys -import time -from pathlib import Path -from typing import Dict, Any, List - -# Add src to path for local development -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from bo_eval_server import EvalServer, EvaluationStack - - -class EvaluationGenerator: - """Helper class for generating evaluations programmatically.""" - - def __init__(self): - self.counter = 0 - - def create_evaluation( - self, - name: str, - tool: str, - input_data: Dict[str, Any], - description: str = "", - metadata: Dict[str, Any] = None, - timeout: float = 30.0, - ) -> Dict[str, Any]: - """Create a standardized evaluation object.""" - self.counter += 1 - - return { - "id": f"generated_{self.counter:03d}_{int(time.time())}", - "name": name, - "description": description or f"Programmatically generated evaluation: {name}", - "tool": tool, - "input": input_data, - "timeout": timeout, - "metadata": { - "generated": True, - "timestamp": time.time(), - "generator": "programmatic_evals.py", - **(metadata or {}) - } - } - - def create_chat_evaluation( - self, - message: str, - name: str = None, - **kwargs - ) -> Dict[str, Any]: - """Create a chat-based evaluation.""" - return self.create_evaluation( - name=name or f"Chat: {message[:30]}...", - tool="chat", - input_data={"message": message}, - **kwargs - ) - - def create_action_evaluation( - self, - objective: str, - url: str = None, - name: str = None, - **kwargs - ) -> Dict[str, Any]: - """Create an action-based evaluation.""" - input_data = {"objective": objective} - if url: - input_data["url"] = url - - return self.create_evaluation( - name=name or f"Action: {objective[:30]}...", - tool="action", - input_data=input_data, - **kwargs - ) - - def create_research_evaluation( - self, - query: str, - depth: str = "basic", - name: str = None, - **kwargs - ) -> Dict[str, Any]: - """Create a research-based evaluation.""" - return self.create_evaluation( - name=name or f"Research: {query[:30]}...", - tool="research", - input_data={ - "query": query, - "depth": depth, - }, - **kwargs - ) - - -def create_dynamic_evaluations(generator: EvaluationGenerator) -> List[Dict[str, Any]]: - """Create evaluations based on dynamic logic.""" - evaluations = [] - - # Math evaluations with increasing difficulty - for i in range(3): - if i == 0: - a, b = random.randint(1, 10), random.randint(1, 10) - op = "+" - difficulty = "easy" - elif i == 1: - a, b = random.randint(10, 50), random.randint(10, 50) - op = "*" - difficulty = "medium" - else: - a, b = random.randint(100, 1000), random.randint(2, 20) - op = "/" - difficulty = "hard" - - evaluation = generator.create_chat_evaluation( - message=f"Calculate: {a} {op} {b}", - name=f"Math {difficulty.title()} #{i+1}", - metadata={ - "category": "mathematics", - "difficulty": difficulty, - "numbers": [a, b], - "operation": op - } - ) - evaluations.append(evaluation) - - # Conditional evaluations based on current time - current_hour = time.localtime().tm_hour - if 6 <= current_hour < 12: - time_context = "morning" - questions = [ - "What's a good breakfast recipe?", - "How can I boost my energy in the morning?", - ] - elif 12 <= current_hour < 18: - time_context = "afternoon" - questions = [ - "What's a healthy lunch option?", - "How can I stay productive in the afternoon?", - ] - else: - time_context = "evening" - questions = [ - "What's a good dinner recipe?", - "How can I relax in the evening?", - ] - - for i, question in enumerate(questions): - evaluation = generator.create_chat_evaluation( - message=question, - name=f"{time_context.title()} Question #{i+1}", - metadata={ - "category": "lifestyle", - "time_context": time_context, - "hour": current_hour - } - ) - evaluations.append(evaluation) - - # Generate research evaluations for trending topics - trending_topics = [ - "artificial intelligence trends 2024", - "sustainable energy solutions", - "space exploration recent developments", - ] - - for topic in trending_topics: - evaluation = generator.create_research_evaluation( - query=topic, - depth="detailed", - name=f"Research: {topic.title()}", - metadata={ - "category": "research", - "topic": topic, - "priority": "high" - }, - timeout=60.0 # Longer timeout for research - ) - evaluations.append(evaluation) - - return evaluations - - -async def main(): - """Main example function for programmatic evaluation creation.""" - print("🏭 Programmatic Evaluation Generation Example") - print("=" * 50) - - # Create evaluation generator - generator = EvaluationGenerator() - - # Create evaluation stack - stack = EvaluationStack() - - # Generate static evaluations - print("\n📝 Creating static evaluations...") - static_evals = [ - generator.create_chat_evaluation( - message="Explain quantum computing in simple terms", - name="Quantum Computing Explanation", - metadata={"category": "science", "complexity": "advanced"} - ), - generator.create_action_evaluation( - objective="Find and click the search button", - url="https://www.google.com", - name="Google Search Action", - metadata={"category": "web_automation", "site": "google"} - ), - generator.create_chat_evaluation( - message="Write a haiku about programming", - name="Programming Haiku", - metadata={"category": "creative", "format": "poetry"} - ), - ] - - for eval_obj in static_evals: - stack.push(eval_obj) - print(f" ➕ {eval_obj['name']}") - - # Generate dynamic evaluations - print("\n🎲 Creating dynamic evaluations...") - dynamic_evals = create_dynamic_evaluations(generator) - - for eval_obj in dynamic_evals: - stack.push(eval_obj) - print(f" ➕ {eval_obj['name']} (category: {eval_obj['metadata']['category']})") - - print(f"\n📊 Total evaluations created: {stack.size()}") - - # Create server - server = EvalServer( - auth_key='programmatic-demo', - host='127.0.0.1', - port=8080, - log_level='INFO', - log_dir='./logs', - max_concurrent_evaluations=5, # Allow more concurrent evaluations - ) - - # Track evaluation results with detailed analysis - results = { - 'completed': [], - 'failed': [], - 'by_category': {}, - 'by_difficulty': {}, - 'timing': [], - } - - @server.on_connect - async def handle_client(client): - print(f'\n🔗 Client connected: {client.id}') - print(f' Processing {stack.size()} evaluations...') - - start_time = time.time() - processed = 0 - - while not stack.is_empty(): - evaluation = stack.pop() - if not evaluation: - break - - processed += 1 - eval_start = time.time() - - print(f'\n📋 [{processed}] {evaluation["name"]}') - print(f' Category: {evaluation["metadata"].get("category", "unknown")}') - print(f' Tool: {evaluation["tool"]}') - - try: - # Use concurrency-limited evaluation - result = await server.evaluate_with_concurrency_limit( - client, - evaluation, - timeout=evaluation.get("timeout", 30.0) - ) - - eval_duration = time.time() - eval_start - - # Record successful result - result_record = { - 'evaluation': evaluation, - 'result': result, - 'duration': eval_duration, - 'client_id': client.id, - 'timestamp': time.time(), - } - results['completed'].append(result_record) - - # Update category stats - category = evaluation["metadata"].get("category", "unknown") - if category not in results['by_category']: - results['by_category'][category] = {'completed': 0, 'failed': 0} - results['by_category'][category]['completed'] += 1 - - # Update difficulty stats - difficulty = evaluation["metadata"].get("difficulty", "unknown") - if difficulty not in results['by_difficulty']: - results['by_difficulty'][difficulty] = {'completed': 0, 'failed': 0} - results['by_difficulty'][difficulty]['completed'] += 1 - - # Record timing - results['timing'].append(eval_duration) - - print(f' ✅ Completed in {eval_duration:.2f}s') - - # Show preview of response - if "output" in result and "response" in result["output"]: - response = result["output"]["response"] - preview = response[:150] + "..." if len(response) > 150 else response - print(f' đŸ’Ŧ "{preview}"') - - except Exception as e: - eval_duration = time.time() - eval_start - - # Record failed result - failure_record = { - 'evaluation': evaluation, - 'error': str(e), - 'duration': eval_duration, - 'client_id': client.id, - 'timestamp': time.time(), - } - results['failed'].append(failure_record) - - # Update stats - category = evaluation["metadata"].get("category", "unknown") - if category not in results['by_category']: - results['by_category'][category] = {'completed': 0, 'failed': 0} - results['by_category'][category]['failed'] += 1 - - difficulty = evaluation["metadata"].get("difficulty", "unknown") - if difficulty not in results['by_difficulty']: - results['by_difficulty'][difficulty] = {'completed': 0, 'failed': 0} - results['by_difficulty'][difficulty]['failed'] += 1 - - print(f' ❌ Failed after {eval_duration:.2f}s: {e}') - - total_duration = time.time() - start_time - print(f'\n🏁 Batch completed in {total_duration:.2f}s') - print(f' Processed: {processed}') - print(f' Success rate: {len(results["completed"])/processed*100:.1f}%') - - # Send detailed completion message - await client.send_message({ - "type": "batch_analysis", - "total_processed": processed, - "completed": len(results['completed']), - "failed": len(results['failed']), - "duration": total_duration, - "average_eval_time": sum(results['timing']) / len(results['timing']) if results['timing'] else 0, - "categories": list(results['by_category'].keys()), - }) - - @server.on_disconnect - async def handle_disconnect(client_info): - print(f'\n🔌 Client disconnected: {client_info["id"]}') - - # Show detailed analysis - total = len(results['completed']) + len(results['failed']) - if total > 0: - print(f'\n📈 Final Analysis:') - print(f' Total evaluations: {total}') - print(f' Successful: {len(results["completed"])} ({len(results["completed"])/total*100:.1f}%)') - print(f' Failed: {len(results["failed"])} ({len(results["failed"])/total*100:.1f}%)') - - if results['timing']: - avg_time = sum(results['timing']) / len(results['timing']) - min_time = min(results['timing']) - max_time = max(results['timing']) - print(f' Average time: {avg_time:.2f}s (min: {min_time:.2f}s, max: {max_time:.2f}s)') - - print(f'\n📊 By Category:') - for category, stats in results['by_category'].items(): - total_cat = stats['completed'] + stats['failed'] - success_rate = stats['completed'] / total_cat * 100 if total_cat > 0 else 0 - print(f' {category}: {total_cat} total, {success_rate:.1f}% success') - - if any(results['by_difficulty'].values()): - print(f'\nđŸŽ¯ By Difficulty:') - for difficulty, stats in results['by_difficulty'].items(): - if difficulty != "unknown": - total_diff = stats['completed'] + stats['failed'] - success_rate = stats['completed'] / total_diff * 100 if total_diff > 0 else 0 - print(f' {difficulty}: {total_diff} total, {success_rate:.1f}% success') - - # Start server - try: - await server.start() - print(f'\n🚀 Server running on ws://{server.config.host}:{server.config.port}') - print(' Connect your agent client to start processing evaluations') - print(' Press Ctrl+C to stop the server') - - # Keep server running - await server.wait_closed() - - except KeyboardInterrupt: - print('\n🛑 Received interrupt signal, stopping server...') - await server.stop() - print('✅ Server stopped successfully') - - except Exception as e: - print(f'đŸ’Ĩ Server error: {e}') - if server.is_running(): - await server.stop() - - -if __name__ == "__main__": - # Ensure logs directory exists - Path("./logs").mkdir(exist_ok=True) - - try: - asyncio.run(main()) - except KeyboardInterrupt: - print('\n👋 Goodbye!') - except Exception as e: - print(f'đŸ’Ĩ Fatal error: {e}') - sys.exit(1) \ No newline at end of file diff --git a/eval-server/python/examples/with_stack.py b/eval-server/python/examples/with_stack.py deleted file mode 100644 index f4b5d20f42..0000000000 --- a/eval-server/python/examples/with_stack.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -""" -EvalServer with EvaluationStack example. - -This example demonstrates using an EvaluationStack to queue evaluations -and distribute them across multiple client connections. -""" - -import asyncio -import sys -from pathlib import Path - -# Add src to path for local development -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) - -from bo_eval_server import EvalServer, EvaluationStack - - -def create_sample_evaluations(): - """Create evaluations matching NodeJS multiple-evals.js exactly.""" - evaluations = [ - { - "id": "math_eval", - "name": "Basic Math Problem", - "description": "Simple arithmetic evaluation", - "tool": "chat", - "input": { - "message": "What is 15 * 7 + 23? Please show your calculation steps." - } - }, - { - "id": "geography_eval", - "name": "Capital of France", - "description": "Geography knowledge test", - "tool": "chat", - "input": { - "message": "What is the capital of France?" - } - }, - { - "id": "creative_eval", - "name": "Creative Writing", - "description": "Short creative writing task", - "tool": "chat", - "input": { - "message": "Write a two-sentence story about a robot discovering friendship." - } - }, - { - "id": "tech_eval", - "name": "Technology Knowledge", - "description": "Basic technology concepts", - "tool": "chat", - "input": { - "message": "Explain what HTTP stands for and what it's used for in simple terms." - } - } - ] - return evaluations - - -async def main(): - """Main example function for evaluation stack usage.""" - # Create evaluation stack and populate it - stack = EvaluationStack() - sample_evaluations = create_sample_evaluations() - - print(f"📚 Created {len(sample_evaluations)} sample evaluations") - - # Add evaluations to stack (LIFO order) - for evaluation in sample_evaluations: - stack.push(evaluation) - print(f" ➕ Added: {evaluation['name']}") - - print(f"📊 Stack size: {stack.size()}") - print(f"🔝 Top evaluation: {stack.peek()['name'] if stack.peek() else 'None'}") - - # Create server - server = EvalServer( - auth_key='stack-demo', - host='127.0.0.1', - port=8080, - log_level='INFO', - log_dir='./logs', - ) - - # Track processed evaluations - completed_evaluations = [] - failed_evaluations = [] - - @server.on_connect - async def handle_client(client): - print('🎉 CLIENT CONNECTED!') - print(f' - Client ID: {client.id}') - print(f' - Client tabId: {client.tab_id}') - print(f' - Client info: {client.get_info()}') - - # Check if we have evaluations left in the stack - if stack.is_empty(): - print('âš ī¸ No more evaluations in stack for this client') - print(' Consider refilling the stack or handling this scenario') - return - - # Pop the next evaluation from the stack (ONE evaluation per client!) - evaluation = stack.pop() - print(f'📋 Assigning evaluation: "{evaluation["name"]}" ({evaluation["id"]})') - print(f'📊 Remaining evaluations in stack: {stack.size()}') - - try: - print('🔄 Starting evaluation...') - result = await client.evaluate(evaluation) - - print('✅ Evaluation completed!') - print(f'📊 Response for "{evaluation["name"]}": {result}') - - completed_evaluations.append({ - 'client_id': client.id, - 'evaluation': evaluation, - 'result': result, - }) - - except Exception as e: - print(f'❌ Evaluation "{evaluation["name"]}" failed: {e}') - - failed_evaluations.append({ - 'client_id': client.id, - 'evaluation': evaluation, - 'error': str(e), - }) - - # Send completion message - try: - await client.send_message({ - "type": "evaluation_complete", - "evaluation_id": evaluation["id"], - "evaluation_name": evaluation["name"], - "status": "completed" if evaluation["id"] not in [e['evaluation']['id'] for e in failed_evaluations] else "failed" - }) - except Exception as e: - print(f' âš ī¸ Failed to send completion message: {e}') - - @server.on_disconnect - async def handle_disconnect(client_info): - print(f'\n🔌 Client disconnected: {client_info["id"]}') - - # Show final statistics - total_completed = len(completed_evaluations) - total_failed = len(failed_evaluations) - remaining = stack.size() - - print(f'\n📊 Final Statistics:') - print(f' ✅ Completed: {total_completed}') - print(f' ❌ Failed: {total_failed}') - print(f' 📚 Remaining: {remaining}') - - if completed_evaluations: - print(f'\nđŸŽ¯ Completed Evaluations:') - for item in completed_evaluations: - eval_name = item['evaluation']['name'] - client_id = item['client_id'][:8] # Short client ID - print(f' â€ĸ {eval_name} (client: {client_id})') - - if failed_evaluations: - print(f'\nđŸ’Ĩ Failed Evaluations:') - for item in failed_evaluations: - eval_name = item['evaluation']['name'] - error = item['error'] - print(f' â€ĸ {eval_name}: {error}') - - # Start server - try: - await server.start() - print(f'\n🚀 Server running on ws://{server.config.host}:{server.config.port}') - print(' Connect your agent client to start processing evaluations') - print(' Press Ctrl+C to stop the server') - - # Keep server running - await server.wait_closed() - - except KeyboardInterrupt: - print('\n🛑 Received interrupt signal, stopping server...') - await server.stop() - print('✅ Server stopped successfully') - - except Exception as e: - print(f'đŸ’Ĩ Server error: {e}') - if server.is_running(): - await server.stop() - - -if __name__ == "__main__": - # Ensure logs directory exists - Path("./logs").mkdir(exist_ok=True) - - try: - asyncio.run(main()) - except KeyboardInterrupt: - print('\n👋 Goodbye!') - except Exception as e: - print(f'đŸ’Ĩ Fatal error: {e}') - sys.exit(1) \ No newline at end of file diff --git a/eval-server/python/logs/.gitignore b/eval-server/python/logs/.gitignore deleted file mode 100644 index 326f777efe..0000000000 --- a/eval-server/python/logs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.log -*.jsonl \ No newline at end of file diff --git a/eval-server/python/pyproject.toml b/eval-server/python/pyproject.toml deleted file mode 100644 index 83d30eea06..0000000000 --- a/eval-server/python/pyproject.toml +++ /dev/null @@ -1,84 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "bo-eval-server" -version = "1.0.0" -description = "WebSocket server for evaluating LLM agents - Python implementation" -readme = "README.md" -license = {text = "MIT"} -authors = [ - {name = "Browser Operator Team"} -] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: System :: Networking", -] -keywords = ["websocket", "llm", "evaluation", "rpc", "library", "programmatic"] -requires-python = ">=3.8" -dependencies = [ - "websockets>=11.0.0", - "loguru>=0.7.0", - "pandas>=2.0.0", - "requests>=2.31.0", -] - -[project.optional-dependencies] -dev = [ - "pytest>=7.0.0", - "pytest-asyncio>=0.21.0", - "black>=23.0.0", - "mypy>=1.0.0", -] - -[project.urls] -Homepage = "https://github.com/chromium/devtools-frontend" -Repository = "https://github.com/chromium/devtools-frontend" -Issues = "https://github.com/chromium/devtools-frontend/issues" - -[project.scripts] -bo-eval-basic = "scripts:run_basic_server" -bo-eval-stack = "scripts:run_with_stack" -bo-eval-programmatic = "scripts:run_programmatic_evals" - -[tool.setuptools.packages.find] -where = ["src"] - -[tool.setuptools.package-data] -"*" = ["*.md", "*.txt", "*.yaml", "*.json"] - -[tool.black] -line-length = 88 -target-version = ['py38'] - -[tool.mypy] -python_version = "3.8" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true - -[tool.pytest.ini_options] -asyncio_mode = "auto" -testpaths = ["tests"] -python_files = ["test_*.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] - -[dependency-groups] -dev = [ - "black>=24.8.0", - "mypy>=1.14.1", - "pytest>=8.3.5", - "pytest-asyncio>=0.24.0", -] diff --git a/eval-server/python/quick_test.py b/eval-server/python/quick_test.py deleted file mode 100644 index 5bf5b9a877..0000000000 --- a/eval-server/python/quick_test.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -"""Quick test to see what's happening with the server.""" - -import asyncio -import json -import websockets - -async def test_server(): - print("🔗 Testing server connection...") - try: - async with websockets.connect('ws://127.0.0.1:8080') as ws: - print("✅ Connected to server") - - # Wait for welcome message - print("âŗ Waiting for welcome message...") - welcome = await asyncio.wait_for(ws.recv(), timeout=5.0) - print(f"đŸ“Ĩ Welcome: {welcome}") - - # Send registration - registration = { - "type": "register", - "clientId": "test-client-123", - "secretKey": "hello", - "capabilities": ["chat"] - } - print(f"📤 Sending registration: {json.dumps(registration)}") - await ws.send(json.dumps(registration)) - - # Wait for ack - print("âŗ Waiting for registration ack...") - ack = await asyncio.wait_for(ws.recv(), timeout=5.0) - print(f"đŸ“Ĩ Registration ack: {ack}") - - except Exception as e: - print(f"❌ Error: {e}") - -if __name__ == "__main__": - asyncio.run(test_server()) \ No newline at end of file diff --git a/eval-server/python/requirements.txt b/eval-server/python/requirements.txt deleted file mode 100644 index e9fc8ca9f8..0000000000 --- a/eval-server/python/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Core dependencies -websockets>=11.0.0 -loguru>=0.7.0 - -# Development dependencies (optional) -# Install with: pip install -e ".[dev]" -# pytest>=7.0.0 -# pytest-asyncio>=0.21.0 -# black>=23.0.0 -# mypy>=1.0.0 \ No newline at end of file diff --git a/eval-server/python/run.py b/eval-server/python/run.py deleted file mode 100644 index 407cd6898e..0000000000 --- a/eval-server/python/run.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple script runner for bo-eval-server examples. - -Usage: - python run.py basic # Run basic server example - python run.py stack # Run evaluation stack example - python run.py prog # Run programmatic evaluations example - python run.py all # Show all available examples -""" - -import subprocess -import sys -from pathlib import Path - - -def run_with_uv(script_path: str, description: str): - """Run a Python script using uv.""" - print(f"🚀 {description}") - print(f" Running: uv run python {script_path}") - print("-" * 50) - - try: - # Ensure logs directory exists - logs_dir = Path("logs") - logs_dir.mkdir(exist_ok=True) - - # Run the script with uv - result = subprocess.run([ - "uv", "run", "python", script_path - ], cwd=Path(__file__).parent) - - return result.returncode - - except KeyboardInterrupt: - print("\n🛑 Interrupted by user") - return 130 - except FileNotFoundError: - print("❌ Error: 'uv' command not found. Please install uv first:") - print(" curl -LsSf https://astral.sh/uv/install.sh | sh") - return 1 - except Exception as e: - print(f"đŸ’Ĩ Error running script: {e}") - return 1 - - -def show_examples(): - """Show all available examples.""" - print("📚 Available Examples:") - print() - print("🔧 basic - Basic WebSocket server setup") - print(" Simple server that connects to one client and runs a single evaluation") - print() - print("📚 stack - Evaluation stack usage") - print(" Demonstrates LIFO queue for managing multiple evaluations") - print() - print("🏭 prog - Programmatic evaluation creation") - print(" Advanced example with dynamic evaluation generation and analytics") - print() - print("Usage:") - print(" python run.py basic") - print(" python run.py stack") - print(" python run.py prog") - print() - print("Or with uv directly:") - print(" uv run python examples/basic_server.py") - print(" uv run python examples/with_stack.py") - print(" uv run python examples/programmatic_evals.py") - - -def main(): - """Main entry point.""" - if len(sys.argv) != 2: - print("Usage: python run.py [basic|stack|prog|all]") - print(" python run.py all # Show all examples") - sys.exit(1) - - command = sys.argv[1].lower() - - examples = { - "basic": ("examples/basic_server.py", "Basic WebSocket Server Example"), - "stack": ("examples/with_stack.py", "Evaluation Stack Example"), - "prog": ("examples/programmatic_evals.py", "Programmatic Evaluations Example"), - "programmatic": ("examples/programmatic_evals.py", "Programmatic Evaluations Example"), - } - - if command == "all": - show_examples() - return 0 - elif command in examples: - script_path, description = examples[command] - return run_with_uv(script_path, description) - else: - print(f"❌ Unknown command: {command}") - print("Available commands: basic, stack, prog, all") - return 1 - - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file diff --git a/eval-server/python/scripts.py b/eval-server/python/scripts.py deleted file mode 100644 index b57377d5a1..0000000000 --- a/eval-server/python/scripts.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -""" -Script runner for bo-eval-server examples using uv. - -This module provides entry points for running examples with uv. -""" - -import asyncio -import sys -from pathlib import Path - -# Add the examples directory to path -examples_dir = Path(__file__).parent / "examples" -sys.path.insert(0, str(examples_dir)) - - -def run_basic_server(): - """Run the basic server example.""" - from examples.basic_server import main - try: - asyncio.run(main()) - except KeyboardInterrupt: - print('\n👋 Goodbye!') - except Exception as e: - print(f'đŸ’Ĩ Error: {e}') - sys.exit(1) - - -def run_with_stack(): - """Run the evaluation stack example.""" - from examples.with_stack import main - try: - asyncio.run(main()) - except KeyboardInterrupt: - print('\n👋 Goodbye!') - except Exception as e: - print(f'đŸ’Ĩ Error: {e}') - sys.exit(1) - - -def run_programmatic_evals(): - """Run the programmatic evaluations example.""" - from examples.programmatic_evals import main - try: - asyncio.run(main()) - except KeyboardInterrupt: - print('\n👋 Goodbye!') - except Exception as e: - print(f'đŸ’Ĩ Error: {e}') - sys.exit(1) - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python scripts.py [basic|stack|programmatic]") - sys.exit(1) - - script = sys.argv[1] - if script == "basic": - run_basic_server() - elif script == "stack": - run_with_stack() - elif script == "programmatic": - run_programmatic_evals() - else: - print(f"Unknown script: {script}") - print("Available scripts: basic, stack, programmatic") - sys.exit(1) \ No newline at end of file diff --git a/eval-server/python/src/bo_eval_server/__init__.py b/eval-server/python/src/bo_eval_server/__init__.py deleted file mode 100644 index 3a8b6aad3d..0000000000 --- a/eval-server/python/src/bo_eval_server/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -bo-eval-server: A minimal Python library for WebSocket-based LLM agent evaluation servers. - -This package provides core functionality for: -- WebSocket server for agent connections -- JSON-RPC 2.0 bidirectional communication -- Evaluation stack for managing evaluation queues -- Enhanced logging and client management -""" - -from .eval_server import EvalServer -from .evaluation_stack import EvaluationStack -from .client_manager import ClientManager, ClientProxy -from .rpc_client import RpcClient -from .config import Config -from .logger import setup_logger - -__version__ = "1.0.0" -__author__ = "Browser Operator Team" - -__all__ = [ - "EvalServer", - "EvaluationStack", - "ClientManager", - "ClientProxy", - "RpcClient", - "Config", - "setup_logger", -] \ No newline at end of file diff --git a/eval-server/python/src/bo_eval_server/client_manager.py b/eval-server/python/src/bo_eval_server/client_manager.py deleted file mode 100644 index 280f6760e3..0000000000 --- a/eval-server/python/src/bo_eval_server/client_manager.py +++ /dev/null @@ -1,401 +0,0 @@ -""" -Client management for WebSocket connections. - -Handles client registration, authentication, and provides a proxy interface -for interacting with connected agents. -""" - -import asyncio -import json -import time -import uuid -from typing import Dict, Any, Optional, List, Callable, Awaitable - -import websockets -from loguru import logger - -from .rpc_client import RpcClient, RpcError, RpcTimeoutError -from .logger import log_connection, log_evaluation - - -class ClientProxy: - """Proxy object for interacting with a connected agent.""" - - def __init__( - self, - client_id: str, - websocket: websockets.WebSocketServerProtocol, - rpc_client: RpcClient, - tab_id: Optional[str] = None, - base_client_id: Optional[str] = None, - capabilities: Optional[List[str]] = None, - ): - """ - Initialize client proxy. - - Args: - client_id: Unique client identifier - websocket: WebSocket connection - rpc_client: RPC client for method calls - tab_id: Browser tab ID (if applicable) - base_client_id: Base client ID for grouping - capabilities: List of agent capabilities - """ - self.id = client_id - self.tab_id = tab_id - self.base_client_id = base_client_id or client_id - self.capabilities = capabilities or [] - self._websocket = websocket - self._rpc_client = rpc_client - self._connected_at = time.time() - - async def evaluate( - self, - evaluation: Dict[str, Any], - timeout: Optional[float] = None, - ) -> Dict[str, Any]: - """ - Execute an evaluation on the connected agent. - - Args: - evaluation: Evaluation object with required fields - timeout: Optional timeout override - - Returns: - Evaluation result from the agent - - Raises: - ValueError: If evaluation is invalid - RpcError: If the RPC call fails - RpcTimeoutError: If the call times out - """ - # Validate evaluation object - required_fields = ['id', 'name', 'tool', 'input'] - for field in required_fields: - if field not in evaluation: - raise ValueError(f"Evaluation missing required field: {field}") - - evaluation_id = evaluation['id'] - start_time = time.time() - - try: - # Log evaluation start - log_evaluation( - evaluation_id=evaluation_id, - client_id=self.id, - status="started", - evaluation_name=evaluation.get('name'), - tool=evaluation.get('tool'), - ) - - # Make RPC call to agent - result = await self._rpc_client.call( - method="evaluate", - params=evaluation, - timeout=timeout, - client_id=self.id, - ) - - duration = time.time() - start_time - - # Log evaluation completion - log_evaluation( - evaluation_id=evaluation_id, - client_id=self.id, - status="completed", - duration=duration, - evaluation_name=evaluation.get('name'), - tool=evaluation.get('tool'), - ) - - return result - - except RpcTimeoutError: - duration = time.time() - start_time - log_evaluation( - evaluation_id=evaluation_id, - client_id=self.id, - status="timeout", - duration=duration, - evaluation_name=evaluation.get('name'), - tool=evaluation.get('tool'), - ) - raise - - except Exception as e: - duration = time.time() - start_time - log_evaluation( - evaluation_id=evaluation_id, - client_id=self.id, - status="failed", - duration=duration, - error=str(e), - evaluation_name=evaluation.get('name'), - tool=evaluation.get('tool'), - ) - raise - - async def send_message(self, message: Dict[str, Any]) -> None: - """ - Send a custom message to the connected agent. - - Args: - message: Message object to send - """ - try: - await self._websocket.send(json.dumps(message)) - except Exception as e: - logger.error(f"Failed to send message to client {self.id}: {e}") - raise - - def get_info(self) -> Dict[str, Any]: - """ - Get client information. - - Returns: - Dictionary with client details - """ - return { - 'id': self.id, - 'tab_id': self.tab_id, - 'base_client_id': self.base_client_id, - 'capabilities': self.capabilities, - 'connected_at': self._connected_at, - 'connected': self._rpc_client.is_connected(), - } - - def is_connected(self) -> bool: - """Check if the client is still connected.""" - return self._rpc_client.is_connected() - - def __repr__(self) -> str: - """String representation of the client proxy.""" - return f"ClientProxy(id={self.id}, connected={self.is_connected()})" - - -class ClientManager: - """Manages WebSocket client connections and authentication.""" - - def __init__(self, auth_key: str, rpc_timeout: float = 1500.0): - """ - Initialize client manager. - - Args: - auth_key: Required authentication key for clients - rpc_timeout: Default RPC timeout in seconds - """ - self.auth_key = auth_key - self.rpc_timeout = rpc_timeout - self._clients: Dict[str, ClientProxy] = {} - self._pending_connections: Dict[str, Dict[str, Any]] = {} - - # Event handlers - self._on_connect_handler: Optional[Callable[[ClientProxy], Awaitable[None]]] = None - self._on_disconnect_handler: Optional[Callable[[Dict[str, Any]], Awaitable[None]]] = None - - def on_connect(self, handler: Callable[[ClientProxy], Awaitable[None]]) -> None: - """Set the handler for client connections.""" - self._on_connect_handler = handler - - def on_disconnect(self, handler: Callable[[Dict[str, Any]], Awaitable[None]]) -> None: - """Set the handler for client disconnections.""" - self._on_disconnect_handler = handler - - async def handle_connection(self, websocket: websockets.WebSocketServerProtocol) -> None: - """ - Handle a new WebSocket connection - matches NodeJS EvalServer flow. - - Args: - websocket: WebSocket connection - """ - connection_id = str(uuid.uuid4()) - client_proxy: Optional[ClientProxy] = None - - try: - # Send welcome message immediately (like NodeJS) - welcome_message = { - 'type': 'welcome', - 'serverId': 'python-eval-server-001', - 'version': '1.0.0', - 'timestamp': time.time() - } - logger.debug(f"Sending welcome message to connection {connection_id}") - await websocket.send(json.dumps(welcome_message)) - - # Wait for registration message - client_proxy = await self._authenticate_client(websocket, connection_id) - - if client_proxy: - # Start RPC client - await client_proxy._rpc_client.start() - - # Add to active clients - self._clients[client_proxy.id] = client_proxy - - # Call connection handler - if self._on_connect_handler: - await self._on_connect_handler(client_proxy) - - # Keep connection alive until closed - await client_proxy._rpc_client._message_handler_task - - except websockets.exceptions.ConnectionClosed: - logger.debug(f"WebSocket connection closed: {connection_id}") - except Exception as e: - logger.error(f"Error handling connection {connection_id}: {e}") - finally: - # Clean up on disconnect - if client_proxy: - await self._handle_disconnect(client_proxy) - - async def _authenticate_client( - self, - websocket: websockets.WebSocketServerProtocol, - connection_id: str, - ) -> Optional[ClientProxy]: - """Authenticate and register a client connection - matches NodeJS implementation.""" - try: - logger.debug(f"Waiting for registration message from connection {connection_id}") - # Wait for registration message with timeout - message = await asyncio.wait_for(websocket.recv(), timeout=30.0) - logger.debug(f"Received message from {connection_id}: {message}") - data = json.loads(message) - - if data.get('type') != 'register': - logger.warning(f"Invalid first message from {connection_id}: expected 'register', got '{data.get('type')}'") - await websocket.send(json.dumps({ - 'type': 'registration_ack', - 'status': 'rejected', - 'message': 'First message must be registration' - })) - return None - - # Auto-accept clients like NodeJS does (NodeJS auto-creates client configs) - # For simplicity, we'll accept any client with the correct secret key or no secret key - if 'secretKey' in data: - if data.get('secretKey') != self.auth_key: - logger.warning(f"Invalid auth key from {connection_id}: expected '{self.auth_key}', got '{data.get('secretKey')}'") - await websocket.send(json.dumps({ - 'type': 'registration_ack', - 'clientId': data.get('clientId', str(uuid.uuid4())), - 'status': 'rejected', - 'message': 'Invalid authentication key' - })) - return None - else: - logger.debug(f"Valid secret key provided by {connection_id}") - else: - logger.debug(f"No secret key provided by {connection_id}, accepting anyway") - - client_id = data.get('clientId', str(uuid.uuid4())) - tab_id = data.get('tabId') - base_client_id = data.get('baseClientId') - capabilities = data.get('capabilities', []) - - logger.info(f"Registering client {client_id} from connection {connection_id}") - logger.debug(f"Client capabilities: {capabilities}") - - # Send registration acknowledgment - registration_response = { - 'type': 'registration_ack', - 'clientId': client_id, - 'status': 'accepted', - 'message': 'Client registered successfully' - } - logger.debug(f"Sending registration ack to {client_id}: {registration_response}") - await websocket.send(json.dumps(registration_response)) - - # Wait for ready signal - logger.debug(f"Waiting for ready signal from client {client_id}") - ready_message = await asyncio.wait_for(websocket.recv(), timeout=30.0) - logger.debug(f"Received ready message from {client_id}: {ready_message}") - ready_data = json.loads(ready_message) - - if ready_data.get('type') != 'ready': - logger.warning(f"Invalid ready message from {client_id}: expected 'ready', got '{ready_data.get('type')}'") - await websocket.send(json.dumps({ - 'type': 'error', - 'message': 'Expected ready signal after registration' - })) - return None - - logger.info(f"Client {client_id} is ready for evaluations") - - # Create RPC client and proxy - rpc_client = RpcClient(websocket, self.rpc_timeout) - client_proxy = ClientProxy( - client_id=client_id, - websocket=websocket, - rpc_client=rpc_client, - tab_id=tab_id, - base_client_id=base_client_id, - capabilities=capabilities, - ) - - # Log successful connection - log_connection( - event="connect", - client_id=client_id, - tab_id=tab_id, - base_client_id=base_client_id, - capabilities=capabilities, - ) - - return client_proxy - - except asyncio.TimeoutError: - logger.warning(f"Client registration timeout: {connection_id}") - return None - except json.JSONDecodeError: - logger.warning(f"Invalid JSON in registration: {connection_id}") - return None - except Exception as e: - logger.error(f"Error during client authentication: {e}") - return None - - async def _handle_disconnect(self, client_proxy: ClientProxy) -> None: - """Handle client disconnection cleanup.""" - client_id = client_proxy.id - - # Remove from active clients - self._clients.pop(client_id, None) - - # Stop RPC client - await client_proxy._rpc_client.stop() - - # Get client info for disconnect handler - client_info = client_proxy.get_info() - - # Log disconnection - log_connection( - event="disconnect", - client_id=client_id, - tab_id=client_proxy.tab_id, - base_client_id=client_proxy.base_client_id, - ) - - # Call disconnect handler - if self._on_disconnect_handler: - try: - await self._on_disconnect_handler(client_info) - except Exception as e: - logger.error(f"Error in disconnect handler: {e}") - - def get_clients(self) -> List[ClientProxy]: - """Get list of connected clients.""" - return list(self._clients.values()) - - def get_client(self, client_id: str) -> Optional[ClientProxy]: - """Get a specific client by ID.""" - return self._clients.get(client_id) - - def get_status(self) -> Dict[str, Any]: - """Get client manager status.""" - return { - 'connected_clients': len(self._clients), - 'client_ids': list(self._clients.keys()), - } - - def __repr__(self) -> str: - """String representation of the client manager.""" - return f"ClientManager(clients={len(self._clients)})" \ No newline at end of file diff --git a/eval-server/python/src/bo_eval_server/config.py b/eval-server/python/src/bo_eval_server/config.py deleted file mode 100644 index 46e72b99ac..0000000000 --- a/eval-server/python/src/bo_eval_server/config.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Configuration management for bo-eval-server. - -Handles server configuration with environment variable support. -""" - -import os -from typing import Optional - - -class Config: - """Configuration class for EvalServer with environment variable support.""" - - def __init__( - self, - host: Optional[str] = None, - port: Optional[int] = None, - auth_key: Optional[str] = None, - log_level: Optional[str] = None, - rpc_timeout: Optional[float] = None, - max_concurrent_evaluations: Optional[int] = None, - ): - """ - Initialize configuration with optional overrides. - - Args: - host: Server host (default: localhost) - port: Server port (default: 8080) - auth_key: Authentication key for clients - log_level: Logging level (default: INFO) - rpc_timeout: RPC call timeout in seconds (default: 1500.0) - max_concurrent_evaluations: Max concurrent evaluations (default: 10) - """ - self.host = host or os.getenv('BO_EVAL_SERVER_HOST', 'localhost') - self.port = int(port or os.getenv('BO_EVAL_SERVER_PORT', '8080')) - self.auth_key = auth_key or os.getenv('BO_EVAL_SERVER_AUTH_KEY') - self.log_level = log_level or os.getenv('BO_EVAL_SERVER_LOG_LEVEL', 'INFO') - self.rpc_timeout = float( - rpc_timeout or os.getenv('BO_EVAL_SERVER_RPC_TIMEOUT', '1500.0') - ) - self.max_concurrent_evaluations = int( - max_concurrent_evaluations or - os.getenv('BO_EVAL_SERVER_MAX_CONCURRENT', '10') - ) - - def validate(self) -> None: - """Validate configuration parameters.""" - if not self.auth_key: - raise ValueError("auth_key is required for server authentication") - - if not isinstance(self.port, int) or self.port <= 0 or self.port > 65535: - raise ValueError(f"Invalid port: {self.port}") - - if self.rpc_timeout <= 0: - raise ValueError(f"Invalid RPC timeout: {self.rpc_timeout}") - - if self.max_concurrent_evaluations <= 0: - raise ValueError( - f"Invalid max_concurrent_evaluations: {self.max_concurrent_evaluations}" - ) - - def to_dict(self) -> dict: - """Convert configuration to dictionary.""" - return { - 'host': self.host, - 'port': self.port, - 'auth_key': '***' if self.auth_key else None, # Hide sensitive data - 'log_level': self.log_level, - 'rpc_timeout': self.rpc_timeout, - 'max_concurrent_evaluations': self.max_concurrent_evaluations, - } - - def __repr__(self) -> str: - """String representation of configuration.""" - return f"Config({self.to_dict()})" \ No newline at end of file diff --git a/eval-server/python/src/bo_eval_server/eval_server.py b/eval-server/python/src/bo_eval_server/eval_server.py deleted file mode 100644 index 9f6ccb70f2..0000000000 --- a/eval-server/python/src/bo_eval_server/eval_server.py +++ /dev/null @@ -1,292 +0,0 @@ -""" -EvalServer - Main WebSocket server for LLM agent evaluations. - -A library-first evaluation server that accepts connections from AI agents, -sends them evaluation tasks via RPC calls, and collects their responses. -""" - -import asyncio -from typing import Dict, Any, Optional, Callable, Awaitable, List - -import websockets -from loguru import logger - -from .config import Config -from .client_manager import ClientManager, ClientProxy -from .logger import setup_logger, log_server_event - - -class EvalServer: - """ - Main evaluation server class for managing WebSocket connections and evaluations. - - Example usage: - ```python - server = EvalServer( - auth_key='your-secret-key', - host='127.0.0.1', - port=8080 - ) - - @server.on_connect - async def handle_client(client): - print(f'Client connected: {client.id}') - - result = await client.evaluate({ - "id": "test_eval", - "name": "Test Evaluation", - "tool": "chat", - "input": {"message": "Hello world"} - }) - - print(f'Response: {result}') - - await server.start() - await server.wait_closed() - ``` - """ - - def __init__( - self, - auth_key: str, - host: str = 'localhost', - port: int = 8080, - rpc_timeout: float = 1500.0, - log_level: str = 'INFO', - log_dir: Optional[str] = None, - max_concurrent_evaluations: int = 10, - ): - """ - Initialize the evaluation server. - - Args: - auth_key: Required authentication key for client connections - host: Server host address - port: Server port number - rpc_timeout: Default timeout for RPC calls in seconds - log_level: Logging level (DEBUG, INFO, WARNING, ERROR) - log_dir: Directory for log files (optional) - max_concurrent_evaluations: Maximum concurrent evaluations - """ - # Create and validate configuration - self.config = Config( - host=host, - port=port, - auth_key=auth_key, - log_level=log_level, - rpc_timeout=rpc_timeout, - max_concurrent_evaluations=max_concurrent_evaluations, - ) - self.config.validate() - - # Setup logging - setup_logger( - log_level=self.config.log_level, - log_dir=log_dir, - ) - - # Initialize client manager - self.client_manager = ClientManager( - auth_key=self.config.auth_key, - rpc_timeout=self.config.rpc_timeout, - ) - - # Server state - self._server: Optional[websockets.WebSocketServer] = None - self._running = False - self._start_time: Optional[float] = None - - # Evaluation concurrency control - self._evaluation_semaphore = asyncio.Semaphore( - self.config.max_concurrent_evaluations - ) - - def on_connect(self, handler: Callable[[ClientProxy], Awaitable[None]]) -> Callable: - """ - Decorator to set the client connection handler. - - Args: - handler: Async function to call when a client connects - - Returns: - The handler function (for decorator use) - """ - self.client_manager.on_connect(handler) - return handler - - def on_disconnect(self, handler: Callable[[Dict[str, Any]], Awaitable[None]]) -> Callable: - """ - Decorator to set the client disconnection handler. - - Args: - handler: Async function to call when a client disconnects - - Returns: - The handler function (for decorator use) - """ - self.client_manager.on_disconnect(handler) - return handler - - async def start(self) -> None: - """ - Start the WebSocket server. - - Raises: - RuntimeError: If server is already running - OSError: If unable to bind to the specified host/port - """ - if self._running: - raise RuntimeError("Server is already running") - - try: - logger.info(f"Starting EvalServer on {self.config.host}:{self.config.port}") - - # Start WebSocket server - self._server = await websockets.serve( - self.client_manager.handle_connection, - self.config.host, - self.config.port, - ping_interval=20, - ping_timeout=20, - close_timeout=10, - ) - - self._running = True - self._start_time = asyncio.get_event_loop().time() - - log_server_event( - event="start", - host=self.config.host, - port=self.config.port, - config=self.config.to_dict(), - ) - - logger.info(f"EvalServer started successfully on ws://{self.config.host}:{self.config.port}") - - except Exception as e: - logger.error(f"Failed to start server: {e}") - log_server_event(event="start_failed", error=str(e)) - raise - - async def stop(self) -> None: - """ - Stop the WebSocket server. - - Raises: - RuntimeError: If server is not running - """ - if not self._running: - raise RuntimeError("Server is not running") - - try: - logger.info("Stopping EvalServer...") - - if self._server: - self._server.close() - await self._server.wait_closed() - - self._running = False - self._start_time = None - - log_server_event(event="stop") - logger.info("EvalServer stopped successfully") - - except Exception as e: - logger.error(f"Error stopping server: {e}") - log_server_event(event="stop_failed", error=str(e)) - raise - - async def wait_closed(self) -> None: - """ - Wait for the server to be closed. - - This method blocks until the server is stopped, useful for keeping - the server running in the main program. - """ - if not self._running or not self._server: - return - - try: - await self._server.wait_closed() - except Exception as e: - logger.error(f"Error waiting for server closure: {e}") - - def get_status(self) -> Dict[str, Any]: - """ - Get server status information. - - Returns: - Dictionary with server status details - """ - uptime = None - if self._running and self._start_time: - uptime = asyncio.get_event_loop().time() - self._start_time - - return { - 'running': self._running, - 'host': self.config.host, - 'port': self.config.port, - 'uptime': uptime, - 'config': self.config.to_dict(), - 'clients': self.client_manager.get_status(), - } - - def get_clients(self) -> List[ClientProxy]: - """ - Get list of connected clients. - - Returns: - List of ClientProxy objects - """ - return self.client_manager.get_clients() - - def get_client(self, client_id: str) -> Optional[ClientProxy]: - """ - Get a specific client by ID. - - Args: - client_id: Client identifier - - Returns: - ClientProxy object or None if not found - """ - return self.client_manager.get_client(client_id) - - async def evaluate_with_concurrency_limit( - self, - client: ClientProxy, - evaluation: Dict[str, Any], - timeout: Optional[float] = None, - ) -> Dict[str, Any]: - """ - Execute an evaluation with concurrency limiting. - - Args: - client: Client to execute evaluation on - evaluation: Evaluation object - timeout: Optional timeout override - - Returns: - Evaluation result - """ - async with self._evaluation_semaphore: - return await client.evaluate(evaluation, timeout) - - def is_running(self) -> bool: - """Check if the server is currently running.""" - return self._running - - def __repr__(self) -> str: - """String representation of the server.""" - status = "running" if self._running else "stopped" - return f"EvalServer(status={status}, host={self.config.host}, port={self.config.port})" - - async def __aenter__(self): - """Async context manager entry.""" - await self.start() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async context manager exit.""" - if self._running: - await self.stop() \ No newline at end of file diff --git a/eval-server/python/src/bo_eval_server/evaluation_stack.py b/eval-server/python/src/bo_eval_server/evaluation_stack.py deleted file mode 100644 index 1ad507830b..0000000000 --- a/eval-server/python/src/bo_eval_server/evaluation_stack.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -EvaluationStack - A simple stack-like structure for managing evaluations. - -Provides LIFO (Last In, First Out) access to evaluation objects. -Useful for distributing different evaluations across multiple client connections. -""" - -from typing import Dict, Any, List, Optional - - -class EvaluationStack: - """A LIFO stack for managing evaluation objects.""" - - def __init__(self) -> None: - """Initialize an empty evaluation stack.""" - self._evaluations: List[Dict[str, Any]] = [] - - def push(self, evaluation: Dict[str, Any]) -> None: - """ - Add an evaluation to the top of the stack. - - Args: - evaluation: The evaluation object to add - - Raises: - ValueError: If evaluation is invalid or missing required fields - """ - if not evaluation or not isinstance(evaluation, dict): - raise ValueError('Evaluation must be a valid dictionary') - - # Validate required fields - required_fields = ['id', 'name', 'tool', 'input'] - for field in required_fields: - if field not in evaluation or not evaluation[field]: - raise ValueError(f'Evaluation missing required field: {field}') - - self._evaluations.append(evaluation) - - def pop(self) -> Optional[Dict[str, Any]]: - """ - Remove and return the evaluation from the top of the stack. - - Returns: - The evaluation object, or None if stack is empty - """ - if self._evaluations: - return self._evaluations.pop() - return None - - def is_empty(self) -> bool: - """ - Check if the stack is empty. - - Returns: - True if stack has no evaluations - """ - return len(self._evaluations) == 0 - - def size(self) -> int: - """ - Get the number of evaluations in the stack. - - Returns: - The stack size - """ - return len(self._evaluations) - - def peek(self) -> Optional[Dict[str, Any]]: - """ - Peek at the top evaluation without removing it. - - Returns: - The top evaluation object, or None if stack is empty - """ - if self.is_empty(): - return None - return self._evaluations[-1] - - def clear(self) -> None: - """Clear all evaluations from the stack.""" - self._evaluations.clear() - - def to_array(self) -> List[Dict[str, Any]]: - """ - Get a copy of all evaluations in the stack (top to bottom). - - Returns: - List of evaluation objects from top to bottom - """ - return list(reversed(self._evaluations)) - - def __len__(self) -> int: - """Return the number of evaluations in the stack.""" - return len(self._evaluations) - - def __bool__(self) -> bool: - """Return True if stack has evaluations.""" - return not self.is_empty() - - def __repr__(self) -> str: - """String representation of the stack.""" - return f"EvaluationStack(size={self.size()})" \ No newline at end of file diff --git a/eval-server/python/src/bo_eval_server/logger.py b/eval-server/python/src/bo_eval_server/logger.py deleted file mode 100644 index 8f6e3c5414..0000000000 --- a/eval-server/python/src/bo_eval_server/logger.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -Enhanced logging setup for bo-eval-server using loguru. - -Provides structured logging with JSON formatting and multiple log levels. -""" - -import sys -from pathlib import Path -from typing import Optional, Dict, Any - -from loguru import logger - - -def setup_logger( - log_level: str = "INFO", - log_dir: Optional[str] = None, - enable_json: bool = True, -) -> None: - """ - Setup enhanced logging with loguru. - - Args: - log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) - log_dir: Directory for log files (default: ./logs) - enable_json: Whether to use JSON formatting for structured logs - """ - # Remove default handler - logger.remove() - - # Console handler with colored output - logger.add( - sys.stdout, - level=log_level, - format="{time:YYYY-MM-DD HH:mm:ss} | " - "{level: <8} | " - "{name}:{function}:{line} - " - "{message}", - colorize=True, - ) - - # File handlers if log_dir is specified - if log_dir: - log_path = Path(log_dir) - log_path.mkdir(exist_ok=True) - - # Combined log file - logger.add( - log_path / "combined.log", - level="DEBUG", - format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", - rotation="10 MB", - retention="7 days", - ) - - # Error log file - logger.add( - log_path / "error.log", - level="ERROR", - format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", - rotation="10 MB", - retention="30 days", - ) - - # Structured JSON log for evaluations - if enable_json: - logger.add( - log_path / "evaluations.jsonl", - level="INFO", - format="{message}", - filter=lambda record: record["extra"].get("event_type") == "evaluation", - rotation="10 MB", - retention="30 days", - ) - - -def log_connection(event: str, client_id: str, **kwargs) -> None: - """ - Log connection events with structured data. - - Args: - event: Connection event type (connect, disconnect, ready) - client_id: Client identifier - **kwargs: Additional event data - """ - logger.bind(event_type="connection").info( - f"Connection {event}: {client_id}", - extra={ - "event_type": "connection", - "connection_event": event, - "client_id": client_id, - **kwargs, - } - ) - - -def log_evaluation( - evaluation_id: str, - client_id: str, - status: str, - duration: Optional[float] = None, - **kwargs -) -> None: - """ - Log evaluation events with structured data. - - Args: - evaluation_id: Unique evaluation identifier - client_id: Client that handled the evaluation - status: Evaluation status (started, completed, failed, timeout) - duration: Evaluation duration in seconds - **kwargs: Additional evaluation data - """ - message = f"Evaluation {status}: {evaluation_id} (client: {client_id})" - if duration is not None: - message += f" ({duration:.2f}s)" - - log_data = { - "event_type": "evaluation", - "evaluation_id": evaluation_id, - "client_id": client_id, - "status": status, - "duration": duration, - **kwargs, - } - - logger.bind(event_type="evaluation").info(message, extra=log_data) - - -def log_rpc_call( - method: str, - client_id: str, - call_id: str, - status: str, - duration: Optional[float] = None, - **kwargs -) -> None: - """ - Log RPC call events with structured data. - - Args: - method: RPC method name - client_id: Target client identifier - call_id: RPC call identifier - status: Call status (sent, completed, failed, timeout) - duration: Call duration in seconds - **kwargs: Additional call data - """ - message = f"RPC {status}: {method} -> {client_id} (id: {call_id})" - if duration is not None: - message += f" ({duration:.2f}s)" - - log_data = { - "event_type": "rpc", - "method": method, - "client_id": client_id, - "call_id": call_id, - "status": status, - "duration": duration, - **kwargs, - } - - logger.bind(event_type="rpc").info(message, extra=log_data) - - -def log_server_event(event: str, **kwargs) -> None: - """ - Log server lifecycle events. - - Args: - event: Server event type (start, stop, error) - **kwargs: Additional event data - """ - logger.bind(event_type="server").info( - f"Server {event}", - extra={ - "event_type": "server", - "server_event": event, - **kwargs, - } - ) \ No newline at end of file diff --git a/eval-server/python/src/bo_eval_server/rpc_client.py b/eval-server/python/src/bo_eval_server/rpc_client.py deleted file mode 100644 index 8fc024b30e..0000000000 --- a/eval-server/python/src/bo_eval_server/rpc_client.py +++ /dev/null @@ -1,229 +0,0 @@ -""" -JSON-RPC 2.0 client implementation for calling methods on connected agents. - -Handles request/response correlation, timeouts, and error conditions. -""" - -import asyncio -import json -import time -import uuid -from typing import Dict, Any, Optional, Callable, Awaitable - -import websockets -from loguru import logger - -from .logger import log_rpc_call - - -class RpcError(Exception): - """Exception raised for RPC-related errors.""" - pass - - -class RpcTimeoutError(RpcError): - """Exception raised when RPC call times out.""" - pass - - -class RpcClient: - """JSON-RPC 2.0 client for bidirectional communication with agents.""" - - def __init__(self, websocket: websockets.WebSocketServerProtocol, timeout: float = 1500.0): - """ - Initialize RPC client for a WebSocket connection. - - Args: - websocket: WebSocket connection to the agent - timeout: Default timeout for RPC calls in seconds - """ - self.websocket = websocket - self.timeout = timeout - self._pending_calls: Dict[str, asyncio.Future] = {} - self._message_handler_task: Optional[asyncio.Task] = None - self._closed = False - - async def start(self) -> None: - """Start the RPC client message handler.""" - if self._message_handler_task is None: - self._message_handler_task = asyncio.create_task(self._handle_messages()) - - async def stop(self) -> None: - """Stop the RPC client and cancel pending calls.""" - self._closed = True - - # Cancel message handler - if self._message_handler_task: - self._message_handler_task.cancel() - try: - await self._message_handler_task - except asyncio.CancelledError: - pass - - # Cancel all pending calls - for future in self._pending_calls.values(): - if not future.done(): - future.cancel() - self._pending_calls.clear() - - async def call( - self, - method: str, - params: Optional[Dict[str, Any]] = None, - timeout: Optional[float] = None, - client_id: Optional[str] = None, - ) -> Any: - """ - Make an RPC call to the connected agent. - - Args: - method: RPC method name to call - params: Parameters to pass to the method - timeout: Timeout for this call (uses default if None) - client_id: Client ID for logging purposes - - Returns: - The result returned by the agent - - Raises: - RpcError: If the call fails or returns an error - RpcTimeoutError: If the call times out - ConnectionError: If the WebSocket connection is closed - """ - if self._closed: - raise ConnectionError("RPC client is closed") - - call_id = str(uuid.uuid4()) - call_timeout = timeout or self.timeout - - # Create JSON-RPC 2.0 request - request = { - "jsonrpc": "2.0", - "method": method, - "params": params or {}, - "id": call_id, - } - - # Create future for response - future: asyncio.Future = asyncio.Future() - self._pending_calls[call_id] = future - - start_time = time.time() - - try: - # Log RPC call start - log_rpc_call( - method=method, - client_id=client_id or "unknown", - call_id=call_id, - status="sent", - params=params, - ) - - # Send request - await self.websocket.send(json.dumps(request)) - - # Wait for response with timeout - try: - result = await asyncio.wait_for(future, timeout=call_timeout) - duration = time.time() - start_time - - # Log successful completion - log_rpc_call( - method=method, - client_id=client_id or "unknown", - call_id=call_id, - status="completed", - duration=duration, - ) - - return result - - except asyncio.TimeoutError: - duration = time.time() - start_time - - # Log timeout - log_rpc_call( - method=method, - client_id=client_id or "unknown", - call_id=call_id, - status="timeout", - duration=duration, - ) - - raise RpcTimeoutError(f"RPC call '{method}' timed out after {call_timeout}s") - - except Exception as e: - duration = time.time() - start_time - - # Log failure - log_rpc_call( - method=method, - client_id=client_id or "unknown", - call_id=call_id, - status="failed", - duration=duration, - error=str(e), - ) - - raise - - finally: - # Clean up pending call - self._pending_calls.pop(call_id, None) - - async def _handle_messages(self) -> None: - """Handle incoming WebSocket messages and route RPC responses.""" - try: - async for message in self.websocket: - if self._closed: - break - - try: - await self._process_message(message) - except Exception as e: - logger.error(f"Error processing RPC message: {e}") - - except websockets.exceptions.ConnectionClosed: - logger.debug("WebSocket connection closed in RPC message handler") - except Exception as e: - logger.error(f"Error in RPC message handler: {e}") - finally: - await self.stop() - - async def _process_message(self, message: str) -> None: - """Process a single WebSocket message.""" - try: - data = json.loads(message) - except json.JSONDecodeError as e: - logger.warning(f"Invalid JSON in RPC message: {e}") - return - - # Handle JSON-RPC 2.0 responses - if isinstance(data, dict) and "jsonrpc" in data and "id" in data: - call_id = data["id"] - future = self._pending_calls.get(call_id) - - if future and not future.done(): - if "result" in data: - # Successful response - future.set_result(data["result"]) - elif "error" in data: - # Error response - error = data["error"] - error_msg = f"RPC error {error.get('code', 'unknown')}: {error.get('message', 'Unknown error')}" - future.set_exception(RpcError(error_msg)) - else: - # Invalid response format - future.set_exception(RpcError("Invalid RPC response format")) - else: - logger.warning(f"Received response for unknown or completed call: {call_id}") - - def is_connected(self) -> bool: - """Check if the RPC client is still active.""" - return not self._closed - - def __repr__(self) -> str: - """String representation of the RPC client.""" - status = "connected" if self.is_connected() else "closed" - return f"RpcClient(status={status}, pending_calls={len(self._pending_calls)})" \ No newline at end of file diff --git a/eval-server/python/test_client.py b/eval-server/python/test_client.py deleted file mode 100644 index 37f2520b00..0000000000 --- a/eval-server/python/test_client.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -""" -Test client for debugging connection issues with bo-eval-server. - -This client helps test the WebSocket connection and protocol implementation. -""" - -import asyncio -import json -import sys -import uuid -from pathlib import Path - -# Add src to path for development -sys.path.insert(0, str(Path(__file__).parent / "src")) - -try: - import websockets -except ImportError: - print("❌ websockets not installed. Run: uv add websockets") - sys.exit(1) - - -class TestClient: - """Simple test client for debugging server connections.""" - - def __init__(self, server_url: str = "ws://127.0.0.1:8080", auth_key: str = "hello"): - self.server_url = server_url - self.auth_key = auth_key - self.client_id = str(uuid.uuid4()) - self.websocket = None - - async def connect_and_test(self): - """Connect to server and test the NodeJS-compatible protocol.""" - print(f"🔗 Connecting to {self.server_url}") - print(f" Client ID: {self.client_id}") - print(f" Auth Key: {self.auth_key}") - - try: - # Connect to WebSocket - self.websocket = await websockets.connect( - self.server_url, - ping_interval=20, - ping_timeout=20, - close_timeout=10, - ) - print("✅ WebSocket connection established") - - # Send registration message (NodeJS style) - registration = { - "type": "register", - "clientId": self.client_id, - "secretKey": self.auth_key, - "capabilities": ["chat", "action", "research"] - } - - print("📤 Sending registration message:") - print(f" {json.dumps(registration, indent=2)}") - - await self.websocket.send(json.dumps(registration)) - - # Wait for registration acknowledgment - print("âŗ Waiting for registration acknowledgment...") - response = await asyncio.wait_for(self.websocket.recv(), timeout=10.0) - response_data = json.loads(response) - - print("đŸ“Ĩ Received registration acknowledgment:") - print(f" {json.dumps(response_data, indent=2)}") - - if response_data.get("type") == "registration_ack" and response_data.get("status") == "accepted": - print("✅ Registration successful!") - - # Send ready signal - ready_message = {"type": "ready"} - print("📤 Sending ready signal:") - print(f" {json.dumps(ready_message, indent=2)}") - - await self.websocket.send(json.dumps(ready_message)) - print("✅ Ready signal sent") - - # Listen for RPC calls - print("👂 Listening for RPC calls...") - await self.listen_for_calls() - - elif response_data.get("type") == "error": - print(f"❌ Registration failed: {response_data.get('message')}") - return False - else: - print(f"❓ Unexpected response: {response_data}") - return False - - except asyncio.TimeoutError: - print("⏰ Timeout waiting for server response") - return False - except websockets.exceptions.ConnectionClosed as e: - print(f"🔌 Connection closed: {e}") - return False - except Exception as e: - print(f"đŸ’Ĩ Error during connection: {e}") - return False - finally: - if self.websocket: - await self.websocket.close() - - return True - - async def listen_for_calls(self): - """Listen for RPC calls from the server.""" - try: - async for message in self.websocket: - print(f"\nđŸ“Ĩ Received message: {message}") - - try: - data = json.loads(message) - - if data.get("jsonrpc") == "2.0" and data.get("method") == "evaluate": - print("đŸŽ¯ Received RPC evaluation request") - print(f" ID: {data.get('id')}") - print(f" Params: {json.dumps(data.get('params', {}), indent=2)}") - - # Send mock response - response = { - "jsonrpc": "2.0", - "id": data["id"], - "result": { - "status": "completed", - "output": { - "response": f"Mock response for evaluation {data['params'].get('name', 'unknown')}" - }, - "metadata": { - "client_id": self.client_id, - "test_client": True - } - } - } - - print("📤 Sending mock response:") - print(f" {json.dumps(response, indent=2)}") - - await self.websocket.send(json.dumps(response)) - print("✅ Mock response sent") - else: - print(f"❓ Unknown message type: {data}") - - except json.JSONDecodeError as e: - print(f"❌ Invalid JSON received: {e}") - - except websockets.exceptions.ConnectionClosed: - print("🔌 Connection closed by server") - except Exception as e: - print(f"đŸ’Ĩ Error listening for calls: {e}") - - -async def main(): - """Main test function.""" - print("đŸ§Ē Test Client for bo-eval-server") - print("=" * 40) - - if len(sys.argv) > 1: - server_url = sys.argv[1] - else: - server_url = "ws://127.0.0.1:8080" - - if len(sys.argv) > 2: - auth_key = sys.argv[2] - else: - auth_key = "hello" # Default from examples - - client = TestClient(server_url, auth_key) - - try: - success = await client.connect_and_test() - if success: - print("\n✅ Test completed successfully!") - else: - print("\n❌ Test failed!") - sys.exit(1) - except KeyboardInterrupt: - print("\n🛑 Test interrupted by user") - except Exception as e: - print(f"\nđŸ’Ĩ Test failed with error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - print("Usage: python test_client.py [ws://server:port] [auth_key]") - print("Example: python test_client.py ws://127.0.0.1:8080 hello") - print() - - asyncio.run(main()) \ No newline at end of file diff --git a/eval-server/python/uv.lock b/eval-server/python/uv.lock deleted file mode 100644 index 2da956841a..0000000000 --- a/eval-server/python/uv.lock +++ /dev/null @@ -1,1306 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.8" -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", -] - -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - -[[package]] -name = "black" -version = "24.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, - { name = "packaging", marker = "python_full_version < '3.9'" }, - { name = "pathspec", marker = "python_full_version < '3.9'" }, - { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "tomli", marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810, upload-time = "2024-08-02T17:43:18.405Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092, upload-time = "2024-08-02T17:47:26.911Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529, upload-time = "2024-08-02T17:47:29.109Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443, upload-time = "2024-08-02T17:46:20.306Z" }, - { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012, upload-time = "2024-08-02T17:47:20.33Z" }, - { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080, upload-time = "2024-08-02T17:48:05.467Z" }, - { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143, upload-time = "2024-08-02T17:47:30.247Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774, upload-time = "2024-08-02T17:46:17.837Z" }, - { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503, upload-time = "2024-08-02T17:46:22.654Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132, upload-time = "2024-08-02T17:49:52.843Z" }, - { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665, upload-time = "2024-08-02T17:47:54.479Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458, upload-time = "2024-08-02T17:46:19.384Z" }, - { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109, upload-time = "2024-08-02T17:46:52.97Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322, upload-time = "2024-08-02T17:51:20.203Z" }, - { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108, upload-time = "2024-08-02T17:50:40.824Z" }, - { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786, upload-time = "2024-08-02T17:46:02.939Z" }, - { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754, upload-time = "2024-08-02T17:46:38.603Z" }, - { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706, upload-time = "2024-08-02T17:49:57.606Z" }, - { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429, upload-time = "2024-08-02T17:49:12.764Z" }, - { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488, upload-time = "2024-08-02T17:46:08.067Z" }, - { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721, upload-time = "2024-08-02T17:46:42.637Z" }, - { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504, upload-time = "2024-08-02T17:43:15.747Z" }, -] - -[[package]] -name = "black" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, - { name = "packaging", marker = "python_full_version >= '3.9'" }, - { name = "pathspec", marker = "python_full_version >= '3.9'" }, - { name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, - { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, - { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, -] - -[[package]] -name = "bo-eval-server" -version = "1.0.0" -source = { editable = "." } -dependencies = [ - { name = "loguru" }, - { name = "pandas", version = "2.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pandas", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "requests" }, - { name = "websockets", version = "13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] - -[package.optional-dependencies] -dev = [ - { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "black", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "mypy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest-asyncio", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] - -[package.dev-dependencies] -dev = [ - { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "black", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "mypy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest-asyncio", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] - -[package.metadata] -requires-dist = [ - { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, - { name = "loguru", specifier = ">=0.7.0" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, - { name = "pandas", specifier = ">=2.0.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, - { name = "requests", specifier = ">=2.31.0" }, - { name = "websockets", specifier = ">=11.0.0" }, -] -provides-extras = ["dev"] - -[package.metadata.requires-dev] -dev = [ - { name = "black", specifier = ">=24.8.0" }, - { name = "mypy", specifier = ">=1.14.1" }, - { name = "pytest", specifier = ">=8.3.5" }, - { name = "pytest-asyncio", specifier = ">=0.24.0" }, -] - -[[package]] -name = "certifi" -version = "2025.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fd/f700cfd4ad876def96d2c769d8a32d808b12d1010b6003dc6639157f99ee/charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", size = 198257, upload-time = "2025-05-02T08:33:45.511Z" }, - { url = "https://files.pythonhosted.org/packages/3a/95/6eec4cbbbd119e6a402e3bfd16246785cc52ce64cf21af2ecdf7b3a08e91/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", size = 143453, upload-time = "2025-05-02T08:33:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b3/d4f913660383b3d93dbe6f687a312ea9f7e89879ae883c4e8942048174d4/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", size = 153130, upload-time = "2025-05-02T08:33:50.568Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/7540141529eabc55bf19cc05cd9b61c2078bebfcdbd3e799af99b777fc28/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", size = 145688, upload-time = "2025-05-02T08:33:52.828Z" }, - { url = "https://files.pythonhosted.org/packages/2e/bb/d76d3d6e340fb0967c43c564101e28a78c9a363ea62f736a68af59ee3683/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", size = 147418, upload-time = "2025-05-02T08:33:54.718Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ef/b7c1f39c0dc3808160c8b72e0209c2479393966313bfebc833533cfff9cc/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", size = 150066, upload-time = "2025-05-02T08:33:56.597Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/4e47cc23d2a4a5eb6ed7d6f0f8cda87d753e2f8abc936d5cf5ad2aae8518/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", size = 144499, upload-time = "2025-05-02T08:33:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/d7/9c/efdf59dd46593cecad0548d36a702683a0bdc056793398a9cd1e1546ad21/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", size = 152954, upload-time = "2025-05-02T08:34:00.552Z" }, - { url = "https://files.pythonhosted.org/packages/59/b3/4e8b73f7299d9aaabd7cd26db4a765f741b8e57df97b034bb8de15609002/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", size = 155876, upload-time = "2025-05-02T08:34:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/53/cb/6fa0ccf941a069adce3edb8a1e430bc80e4929f4d43b5140fdf8628bdf7d/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", size = 153186, upload-time = "2025-05-02T08:34:04.481Z" }, - { url = "https://files.pythonhosted.org/packages/ac/c6/80b93fabc626b75b1665ffe405e28c3cef0aae9237c5c05f15955af4edd8/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", size = 148007, upload-time = "2025-05-02T08:34:06.888Z" }, - { url = "https://files.pythonhosted.org/packages/41/eb/c7367ac326a2628e4f05b5c737c86fe4a8eb3ecc597a4243fc65720b3eeb/charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", size = 97923, upload-time = "2025-05-02T08:34:08.792Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/1c82646582ccf2c757fa6af69b1a3ea88744b8d2b4ab93b7686b2533e023/charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", size = 105020, upload-time = "2025-05-02T08:34:10.6Z" }, - { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, - { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, - { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, - { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, - { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, - { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - -[[package]] -name = "click" -version = "8.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "loguru" -version = "0.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, -] - -[[package]] -name = "mypy" -version = "1.14.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -dependencies = [ - { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, - { name = "tomli", marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, - { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, - { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, - { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, - { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, - { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, - { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, - { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, - { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, - { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, - { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, - { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, - { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, - { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, - { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, - { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, - { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, - { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, -] - -[[package]] -name = "mypy" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, - { name = "pathspec", marker = "python_full_version >= '3.9'" }, - { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, - { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, - { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, - { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, - { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, - { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, - { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, - { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" }, - { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" }, - { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" }, - { url = "https://files.pythonhosted.org/packages/14/55/e13d0dcd276975927d1f4e9e2ec4fd409e199f01bdc671717e673cc63a22/mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d", size = 9512564, upload-time = "2025-07-31T07:53:12.346Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - -[[package]] -name = "numpy" -version = "1.24.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/a4/9b/027bec52c633f6556dba6b722d9a0befb40498b9ceddd29cbe67a45a127c/numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463", size = 10911229, upload-time = "2023-06-26T13:39:33.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/80/6cdfb3e275d95155a34659163b83c09e3a3ff9f1456880bec6cc63d71083/numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64", size = 19789140, upload-time = "2023-06-26T13:22:33.184Z" }, - { url = "https://files.pythonhosted.org/packages/64/5f/3f01d753e2175cfade1013eea08db99ba1ee4bdb147ebcf3623b75d12aa7/numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1", size = 13854297, upload-time = "2023-06-26T13:22:59.541Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b3/2f9c21d799fa07053ffa151faccdceeb69beec5a010576b8991f614021f7/numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4", size = 13995611, upload-time = "2023-06-26T13:23:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/10/be/ae5bf4737cb79ba437879915791f6f26d92583c738d7d960ad94e5c36adf/numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6", size = 17282357, upload-time = "2023-06-26T13:23:51.446Z" }, - { url = "https://files.pythonhosted.org/packages/c0/64/908c1087be6285f40e4b3e79454552a701664a079321cff519d8c7051d06/numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc", size = 12429222, upload-time = "2023-06-26T13:24:13.849Z" }, - { url = "https://files.pythonhosted.org/packages/22/55/3d5a7c1142e0d9329ad27cece17933b0e2ab4e54ddc5c1861fbfeb3f7693/numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e", size = 14841514, upload-time = "2023-06-26T13:24:38.129Z" }, - { url = "https://files.pythonhosted.org/packages/a9/cc/5ed2280a27e5dab12994c884f1f4d8c3bd4d885d02ae9e52a9d213a6a5e2/numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810", size = 19775508, upload-time = "2023-06-26T13:25:08.882Z" }, - { url = "https://files.pythonhosted.org/packages/c0/bc/77635c657a3668cf652806210b8662e1aff84b818a55ba88257abf6637a8/numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254", size = 13840033, upload-time = "2023-06-26T13:25:33.417Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4c/96cdaa34f54c05e97c1c50f39f98d608f96f0677a6589e64e53104e22904/numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7", size = 13991951, upload-time = "2023-06-26T13:25:55.725Z" }, - { url = "https://files.pythonhosted.org/packages/22/97/dfb1a31bb46686f09e68ea6ac5c63fdee0d22d7b23b8f3f7ea07712869ef/numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5", size = 17278923, upload-time = "2023-06-26T13:26:25.658Z" }, - { url = "https://files.pythonhosted.org/packages/35/e2/76a11e54139654a324d107da1d98f99e7aa2a7ef97cfd7c631fba7dbde71/numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d", size = 12422446, upload-time = "2023-06-26T13:26:49.302Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ec/ebef2f7d7c28503f958f0f8b992e7ce606fb74f9e891199329d5f5f87404/numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694", size = 14834466, upload-time = "2023-06-26T13:27:16.029Z" }, - { url = "https://files.pythonhosted.org/packages/11/10/943cfb579f1a02909ff96464c69893b1d25be3731b5d3652c2e0cf1281ea/numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61", size = 19780722, upload-time = "2023-06-26T13:27:49.573Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ae/f53b7b265fdc701e663fbb322a8e9d4b14d9cb7b2385f45ddfabfc4327e4/numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f", size = 13843102, upload-time = "2023-06-26T13:28:12.288Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/2586a50ad72e8dbb1d8381f837008a0321a3516dfd7cb57fc8cf7e4bb06b/numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e", size = 14039616, upload-time = "2023-06-26T13:28:35.659Z" }, - { url = "https://files.pythonhosted.org/packages/98/5d/5738903efe0ecb73e51eb44feafba32bdba2081263d40c5043568ff60faf/numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc", size = 17316263, upload-time = "2023-06-26T13:29:09.272Z" }, - { url = "https://files.pythonhosted.org/packages/d1/57/8d328f0b91c733aa9aa7ee540dbc49b58796c862b4fbcb1146c701e888da/numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2", size = 12455660, upload-time = "2023-06-26T13:29:33.434Z" }, - { url = "https://files.pythonhosted.org/packages/69/65/0d47953afa0ad569d12de5f65d964321c208492064c38fe3b0b9744f8d44/numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706", size = 14868112, upload-time = "2023-06-26T13:29:58.385Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cd/d5b0402b801c8a8b56b04c1e85c6165efab298d2f0ab741c2406516ede3a/numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400", size = 19816549, upload-time = "2023-06-26T13:30:36.976Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/638aaa446f39113a3ed38b37a66243e21b38110d021bfcb940c383e120f2/numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f", size = 13879950, upload-time = "2023-06-26T13:31:01.787Z" }, - { url = "https://files.pythonhosted.org/packages/8f/27/91894916e50627476cff1a4e4363ab6179d01077d71b9afed41d9e1f18bf/numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9", size = 14030228, upload-time = "2023-06-26T13:31:26.696Z" }, - { url = "https://files.pythonhosted.org/packages/7a/7c/d7b2a0417af6428440c0ad7cb9799073e507b1a465f827d058b826236964/numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d", size = 17311170, upload-time = "2023-06-26T13:31:56.615Z" }, - { url = "https://files.pythonhosted.org/packages/18/9d/e02ace5d7dfccee796c37b995c63322674daf88ae2f4a4724c5dd0afcc91/numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835", size = 12454918, upload-time = "2023-06-26T13:32:16.8Z" }, - { url = "https://files.pythonhosted.org/packages/63/38/6cc19d6b8bfa1d1a459daf2b3fe325453153ca7019976274b6f33d8b5663/numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8", size = 14867441, upload-time = "2023-06-26T13:32:40.521Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fd/8dff40e25e937c94257455c237b9b6bf5a30d42dd1cc11555533be099492/numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef", size = 19156590, upload-time = "2023-06-26T13:33:10.36Z" }, - { url = "https://files.pythonhosted.org/packages/42/e7/4bf953c6e05df90c6d351af69966384fed8e988d0e8c54dad7103b59f3ba/numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a", size = 16705744, upload-time = "2023-06-26T13:33:36.703Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/9106005eb477d022b60b3817ed5937a43dad8fd1f20b0610ea8a32fcb407/numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2", size = 14734290, upload-time = "2023-06-26T13:34:05.409Z" }, -] - -[[package]] -name = "numpy" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, - { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, - { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, - { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, - { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, - { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, - { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, - { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, - { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, - { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, - { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, - { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, - { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, - { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, - { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, - { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, - { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, - { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, - { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, - { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, - { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, - { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, - { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, - { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, -] - -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.10.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - -[[package]] -name = "numpy" -version = "2.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016, upload-time = "2025-07-24T20:24:35.214Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168", size = 14451158, upload-time = "2025-07-24T20:24:58.397Z" }, - { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b", size = 5379817, upload-time = "2025-07-24T20:25:07.746Z" }, - { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8", size = 6913606, upload-time = "2025-07-24T20:25:18.84Z" }, - { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d", size = 14589652, upload-time = "2025-07-24T20:25:40.356Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3", size = 16938816, upload-time = "2025-07-24T20:26:05.721Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f", size = 16370512, upload-time = "2025-07-24T20:26:30.545Z" }, - { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097", size = 18884947, upload-time = "2025-07-24T20:26:58.24Z" }, - { url = "https://files.pythonhosted.org/packages/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220", size = 6599494, upload-time = "2025-07-24T20:27:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170", size = 13087889, upload-time = "2025-07-24T20:27:29.558Z" }, - { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89", size = 10459560, upload-time = "2025-07-24T20:27:46.803Z" }, - { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, - { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, - { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, - { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, - { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, - { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, - { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, - { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, - { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, - { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, - { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, - { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, - { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, - { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, - { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, - { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, - { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, - { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, - { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, - { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, - { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, - { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, - { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, - { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, - { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, - { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, - { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, - { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, - { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, - { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, - { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, - { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, - { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, - { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, - { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15", size = 21130338, upload-time = "2025-07-24T20:57:54.37Z" }, - { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec", size = 14375776, upload-time = "2025-07-24T20:58:16.303Z" }, - { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712", size = 5304882, upload-time = "2025-07-24T20:58:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c", size = 6818405, upload-time = "2025-07-24T20:58:37.341Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296", size = 14419651, upload-time = "2025-07-24T20:58:59.048Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981", size = 16760166, upload-time = "2025-07-24T21:28:56.38Z" }, - { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "pandas" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -dependencies = [ - { name = "numpy", version = "1.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "python-dateutil", marker = "python_full_version < '3.9'" }, - { name = "pytz", marker = "python_full_version < '3.9'" }, - { name = "tzdata", marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/a7/824332581e258b5aa4f3763ecb2a797e5f9a54269044ba2e50ac19936b32/pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c", size = 5284455, upload-time = "2023-06-28T23:19:33.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/b2/0d4a5729ce1ce11630c4fc5d5522a33b967b3ca146c210f58efde7c40e99/pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8", size = 11760908, upload-time = "2023-06-28T23:15:57.001Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f6/f620ca62365d83e663a255a41b08d2fc2eaf304e0b8b21bb6d62a7390fe3/pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f", size = 10823486, upload-time = "2023-06-28T23:16:06.863Z" }, - { url = "https://files.pythonhosted.org/packages/c2/59/cb4234bc9b968c57e81861b306b10cd8170272c57b098b724d3de5eda124/pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183", size = 11571897, upload-time = "2023-06-28T23:16:14.208Z" }, - { url = "https://files.pythonhosted.org/packages/e3/59/35a2892bf09ded9c1bf3804461efe772836a5261ef5dfb4e264ce813ff99/pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0", size = 12306421, upload-time = "2023-06-28T23:16:23.26Z" }, - { url = "https://files.pythonhosted.org/packages/94/71/3a0c25433c54bb29b48e3155b959ac78f4c4f2f06f94d8318aac612cb80f/pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210", size = 9540792, upload-time = "2023-06-28T23:16:30.876Z" }, - { url = "https://files.pythonhosted.org/packages/ed/30/b97456e7063edac0e5a405128065f0cd2033adfe3716fb2256c186bd41d0/pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e", size = 10664333, upload-time = "2023-06-28T23:16:39.209Z" }, - { url = "https://files.pythonhosted.org/packages/b3/92/a5e5133421b49e901a12e02a6a7ef3a0130e10d13db8cb657fdd0cba3b90/pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8", size = 11645672, upload-time = "2023-06-28T23:16:47.601Z" }, - { url = "https://files.pythonhosted.org/packages/8f/bb/aea1fbeed5b474cb8634364718abe9030d7cc7a30bf51f40bd494bbc89a2/pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26", size = 10693229, upload-time = "2023-06-28T23:16:56.397Z" }, - { url = "https://files.pythonhosted.org/packages/d6/90/e7d387f1a416b14e59290baa7a454a90d719baebbf77433ff1bdcc727800/pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d", size = 11581591, upload-time = "2023-06-28T23:17:04.234Z" }, - { url = "https://files.pythonhosted.org/packages/d0/28/88b81881c056376254618fad622a5e94b5126db8c61157ea1910cd1c040a/pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df", size = 12219370, upload-time = "2023-06-28T23:17:11.783Z" }, - { url = "https://files.pythonhosted.org/packages/e4/a5/212b9039e25bf8ebb97e417a96660e3dc925dacd3f8653d531b8f7fd9be4/pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd", size = 9482935, upload-time = "2023-06-28T23:17:21.376Z" }, - { url = "https://files.pythonhosted.org/packages/9e/71/756a1be6bee0209d8c0d8c5e3b9fc72c00373f384a4017095ec404aec3ad/pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b", size = 10607692, upload-time = "2023-06-28T23:17:28.824Z" }, - { url = "https://files.pythonhosted.org/packages/78/a8/07dd10f90ca915ed914853cd57f79bfc22e1ef4384ab56cb4336d2fc1f2a/pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061", size = 11653303, upload-time = "2023-06-28T23:17:36.329Z" }, - { url = "https://files.pythonhosted.org/packages/53/c3/f8e87361f7fdf42012def602bfa2a593423c729f5cb7c97aed7f51be66ac/pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5", size = 10710932, upload-time = "2023-06-28T23:17:49.875Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/828d50c81ce0f434163bf70b925a0eec6076808e0bca312a79322b141f66/pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089", size = 11684018, upload-time = "2023-06-28T23:18:05.845Z" }, - { url = "https://files.pythonhosted.org/packages/f8/7f/5b047effafbdd34e52c9e2d7e44f729a0655efafb22198c45cf692cdc157/pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0", size = 12353723, upload-time = "2023-06-28T23:18:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ae/26a2eda7fa581347d69e51f93892493b2074ef3352ac71033c9f32c52389/pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02", size = 9646403, upload-time = "2023-06-28T23:18:24.328Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6c/ea362eef61f05553aaf1a24b3e96b2d0603f5dc71a3bd35688a24ed88843/pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78", size = 10777638, upload-time = "2023-06-28T23:18:30.947Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c7/cfef920b7b457dff6928e824896cb82367650ea127d048ee0b820026db4f/pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b", size = 11834160, upload-time = "2023-06-28T23:18:40.332Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/689c9d99bc4e5d366a5fd871f0bcdee98a6581e240f96b78d2d08f103774/pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e", size = 10862752, upload-time = "2023-06-28T23:18:50.016Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b8/4d082f41c27c95bf90485d1447b647cc7e5680fea75e315669dc6e4cb398/pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b", size = 11715852, upload-time = "2023-06-28T23:19:00.594Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0d/91a9fd2c202f2b1d97a38ab591890f86480ecbb596cbc56d035f6f23fdcc/pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641", size = 12398496, upload-time = "2023-06-28T23:19:11.78Z" }, - { url = "https://files.pythonhosted.org/packages/26/7d/d8aa0a2c4f3f5f8ea59fb946c8eafe8f508090ca73e2b08a9af853c1103e/pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682", size = 9630766, upload-time = "2023-06-28T23:19:18.182Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f2/0ad053856debbe90c83de1b4f05915f85fd2146f20faf9daa3b320d36df3/pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc", size = 10755902, upload-time = "2023-06-28T23:19:25.151Z" }, -] - -[[package]] -name = "pandas" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.9'" }, - { name = "pytz", marker = "python_full_version >= '3.9'" }, - { name = "tzdata", marker = "python_full_version >= '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ca/aa97b47287221fa37a49634532e520300088e290b20d690b21ce3e448143/pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9", size = 11542731, upload-time = "2025-07-07T19:18:12.619Z" }, - { url = "https://files.pythonhosted.org/packages/80/bf/7938dddc5f01e18e573dcfb0f1b8c9357d9b5fa6ffdee6e605b92efbdff2/pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1", size = 10790031, upload-time = "2025-07-07T19:18:16.611Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2f/9af748366763b2a494fed477f88051dbf06f56053d5c00eba652697e3f94/pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0", size = 11724083, upload-time = "2025-07-07T19:18:20.512Z" }, - { url = "https://files.pythonhosted.org/packages/2c/95/79ab37aa4c25d1e7df953dde407bb9c3e4ae47d154bc0dd1692f3a6dcf8c/pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191", size = 12342360, upload-time = "2025-07-07T19:18:23.194Z" }, - { url = "https://files.pythonhosted.org/packages/75/a7/d65e5d8665c12c3c6ff5edd9709d5836ec9b6f80071b7f4a718c6106e86e/pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1", size = 13202098, upload-time = "2025-07-07T19:18:25.558Z" }, - { url = "https://files.pythonhosted.org/packages/65/f3/4c1dbd754dbaa79dbf8b537800cb2fa1a6e534764fef50ab1f7533226c5c/pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97", size = 13837228, upload-time = "2025-07-07T19:18:28.344Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d6/d7f5777162aa9b48ec3910bca5a58c9b5927cfd9cfde3aa64322f5ba4b9f/pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83", size = 11336561, upload-time = "2025-07-07T19:18:31.211Z" }, - { url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608, upload-time = "2025-07-07T19:18:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181, upload-time = "2025-07-07T19:18:36.151Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570, upload-time = "2025-07-07T19:18:38.385Z" }, - { url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887, upload-time = "2025-07-07T19:18:41.284Z" }, - { url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957, upload-time = "2025-07-07T19:18:44.187Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883, upload-time = "2025-07-07T19:18:46.498Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212, upload-time = "2025-07-07T19:18:49.293Z" }, - { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" }, - { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" }, - { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" }, - { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" }, - { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" }, - { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" }, - { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" }, - { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" }, - { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" }, - { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" }, - { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, - { url = "https://files.pythonhosted.org/packages/6e/21/ecf2df680982616459409b09962a8c2065330c7151dc6538069f3b634acf/pandas-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4645f770f98d656f11c69e81aeb21c6fca076a44bed3dcbb9396a4311bc7f6d8", size = 11567275, upload-time = "2025-07-07T19:19:45.152Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1a/dcb50e44b75419e96b276c9fb023b0f147b3c411be1cd517492aa2a184d4/pandas-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:342e59589cc454aaff7484d75b816a433350b3d7964d7847327edda4d532a2e3", size = 10811488, upload-time = "2025-07-07T19:19:47.797Z" }, - { url = "https://files.pythonhosted.org/packages/2d/55/66cd2b679f6a27398380eac7574bc24746128f74626a3c02b978ea00e5ce/pandas-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d12f618d80379fde6af007f65f0c25bd3e40251dbd1636480dfffce2cf1e6da", size = 11763000, upload-time = "2025-07-07T19:19:50.83Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1c/5b9b263c80fd5e231b77df6f78cd7426d1d4ad3a4e858e85b7b3d93d0e9c/pandas-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd71c47a911da120d72ef173aeac0bf5241423f9bfea57320110a978457e069e", size = 12361395, upload-time = "2025-07-07T19:19:53.714Z" }, - { url = "https://files.pythonhosted.org/packages/f7/74/7e817b31413fbb96366ea327d43d1926a9c48c58074e27e094e2839a0e36/pandas-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09e3b1587f0f3b0913e21e8b32c3119174551deb4a4eba4a89bc7377947977e7", size = 13225086, upload-time = "2025-07-07T19:19:56.378Z" }, - { url = "https://files.pythonhosted.org/packages/1f/0f/bc0a44b47eba2f22ae4235719a573d552ef7ad76ed3ea39ae62d554e040b/pandas-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2323294c73ed50f612f67e2bf3ae45aea04dce5690778e08a09391897f35ff88", size = 13871698, upload-time = "2025-07-07T19:19:58.854Z" }, - { url = "https://files.pythonhosted.org/packages/fa/cb/6c32f8fadefa4314b740fbe8f74f6a02423bd1549e7c930826df35ac3c1b/pandas-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4b0de34dc8499c2db34000ef8baad684cfa4cbd836ecee05f323ebfba348c7d", size = 11357186, upload-time = "2025-07-07T19:20:01.475Z" }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.3.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pytest" -version = "8.3.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, - { name = "iniconfig", marker = "python_full_version < '3.9'" }, - { name = "packaging", marker = "python_full_version < '3.9'" }, - { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "tomli", marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "iniconfig", marker = "python_full_version >= '3.9'" }, - { name = "packaging", marker = "python_full_version >= '3.9'" }, - { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "pygments", marker = "python_full_version >= '3.9'" }, - { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -dependencies = [ - { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855, upload-time = "2024-08-22T08:03:18.145Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024, upload-time = "2024-08-22T08:03:15.536Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, -] - -[[package]] -name = "requests" -version = "2.32.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.14.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, -] - -[[package]] -name = "tzdata" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] - -[[package]] -name = "websockets" -version = "13.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815, upload-time = "2024-09-21T17:32:27.107Z" }, - { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466, upload-time = "2024-09-21T17:32:28.428Z" }, - { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716, upload-time = "2024-09-21T17:32:29.905Z" }, - { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806, upload-time = "2024-09-21T17:32:31.384Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810, upload-time = "2024-09-21T17:32:32.384Z" }, - { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125, upload-time = "2024-09-21T17:32:33.398Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532, upload-time = "2024-09-21T17:32:35.109Z" }, - { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948, upload-time = "2024-09-21T17:32:36.214Z" }, - { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898, upload-time = "2024-09-21T17:32:37.277Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706, upload-time = "2024-09-21T17:32:38.755Z" }, - { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141, upload-time = "2024-09-21T17:32:40.495Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813, upload-time = "2024-09-21T17:32:42.188Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469, upload-time = "2024-09-21T17:32:43.858Z" }, - { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717, upload-time = "2024-09-21T17:32:44.914Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379, upload-time = "2024-09-21T17:32:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376, upload-time = "2024-09-21T17:32:46.987Z" }, - { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753, upload-time = "2024-09-21T17:32:48.046Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051, upload-time = "2024-09-21T17:32:49.271Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489, upload-time = "2024-09-21T17:32:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438, upload-time = "2024-09-21T17:32:52.223Z" }, - { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710, upload-time = "2024-09-21T17:32:53.244Z" }, - { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137, upload-time = "2024-09-21T17:32:54.721Z" }, - { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821, upload-time = "2024-09-21T17:32:56.442Z" }, - { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480, upload-time = "2024-09-21T17:32:57.698Z" }, - { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715, upload-time = "2024-09-21T17:32:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647, upload-time = "2024-09-21T17:33:00.495Z" }, - { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592, upload-time = "2024-09-21T17:33:02.223Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012, upload-time = "2024-09-21T17:33:03.288Z" }, - { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311, upload-time = "2024-09-21T17:33:04.728Z" }, - { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692, upload-time = "2024-09-21T17:33:05.829Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686, upload-time = "2024-09-21T17:33:06.823Z" }, - { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712, upload-time = "2024-09-21T17:33:07.877Z" }, - { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145, upload-time = "2024-09-21T17:33:09.202Z" }, - { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828, upload-time = "2024-09-21T17:33:10.987Z" }, - { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487, upload-time = "2024-09-21T17:33:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721, upload-time = "2024-09-21T17:33:13.909Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609, upload-time = "2024-09-21T17:33:14.967Z" }, - { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556, upload-time = "2024-09-21T17:33:17.113Z" }, - { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993, upload-time = "2024-09-21T17:33:18.168Z" }, - { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360, upload-time = "2024-09-21T17:33:19.233Z" }, - { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745, upload-time = "2024-09-21T17:33:20.361Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, - { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, - { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/59872420e5bce60db166d6fba39ee24c719d339fb0ae48cb2ce580129882/websockets-13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d", size = 157811, upload-time = "2024-09-21T17:33:27.379Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f7/0610032e0d3981758fdd6ee7c68cc02ebf668a762c5178d3d91748228849/websockets-13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23", size = 155471, upload-time = "2024-09-21T17:33:28.473Z" }, - { url = "https://files.pythonhosted.org/packages/55/2f/c43173a72ea395263a427a36d25bce2675f41c809424466a13c61a9a2d61/websockets-13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c", size = 155713, upload-time = "2024-09-21T17:33:29.795Z" }, - { url = "https://files.pythonhosted.org/packages/92/7e/8fa930c6426a56c47910792717787640329e4a0e37cdfda20cf89da67126/websockets-13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea", size = 164995, upload-time = "2024-09-21T17:33:30.802Z" }, - { url = "https://files.pythonhosted.org/packages/27/29/50ed4c68a3f606565a2db4b13948ae7b6f6c53aa9f8f258d92be6698d276/websockets-13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7", size = 164057, upload-time = "2024-09-21T17:33:31.862Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0e/60da63b1c53c47f389f79312b3356cb305600ffad1274d7ec473128d4e6b/websockets-13.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54", size = 164340, upload-time = "2024-09-21T17:33:33.022Z" }, - { url = "https://files.pythonhosted.org/packages/20/ef/d87c5fc0aa7fafad1d584b6459ddfe062edf0d0dd64800a02e67e5de048b/websockets-13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db", size = 164222, upload-time = "2024-09-21T17:33:34.423Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c4/7916e1f6b5252d3dcb9121b67d7fdbb2d9bf5067a6d8c88885ba27a9e69c/websockets-13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295", size = 163647, upload-time = "2024-09-21T17:33:35.841Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/2ebebb807f10993c35c10cbd3628a7944b66bd5fb6632a561f8666f3a68e/websockets-13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96", size = 163590, upload-time = "2024-09-21T17:33:37.61Z" }, - { url = "https://files.pythonhosted.org/packages/b5/82/d48911f56bb993c11099a1ff1d4041d9d1481d50271100e8ee62bc28f365/websockets-13.1-cp38-cp38-win32.whl", hash = "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf", size = 158701, upload-time = "2024-09-21T17:33:38.695Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b3/945aacb21fc89ad150403cbaa974c9e846f098f16d9f39a3dd6094f9beb1/websockets-13.1-cp38-cp38-win_amd64.whl", hash = "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6", size = 159146, upload-time = "2024-09-21T17:33:39.855Z" }, - { url = "https://files.pythonhosted.org/packages/61/26/5f7a7fb03efedb4f90ed61968338bfe7c389863b0ceda239b94ae61c5ae4/websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d", size = 157810, upload-time = "2024-09-21T17:33:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d4/9b4814a07dffaa7a79d71b4944d10836f9adbd527a113f6675734ef3abed/websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7", size = 155467, upload-time = "2024-09-21T17:33:42.075Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1a/2abdc7ce3b56429ae39d6bfb48d8c791f5a26bbcb6f44aabcf71ffc3fda2/websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a", size = 155714, upload-time = "2024-09-21T17:33:43.128Z" }, - { url = "https://files.pythonhosted.org/packages/2a/98/189d7cf232753a719b2726ec55e7922522632248d5d830adf078e3f612be/websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa", size = 164587, upload-time = "2024-09-21T17:33:44.27Z" }, - { url = "https://files.pythonhosted.org/packages/a5/2b/fb77cedf3f9f55ef8605238c801eef6b9a5269b01a396875a86896aea3a6/websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa", size = 163588, upload-time = "2024-09-21T17:33:45.38Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b7/070481b83d2d5ac0f19233d9f364294e224e6478b0762f07fa7f060e0619/websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79", size = 163894, upload-time = "2024-09-21T17:33:46.651Z" }, - { url = "https://files.pythonhosted.org/packages/eb/be/d6e1cff7d441cfe5eafaacc5935463e5f14c8b1c0d39cb8afde82709b55a/websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17", size = 164315, upload-time = "2024-09-21T17:33:48.432Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5e/ffa234473e46ab2d3f9fd9858163d5db3ecea1439e4cb52966d78906424b/websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6", size = 163714, upload-time = "2024-09-21T17:33:49.548Z" }, - { url = "https://files.pythonhosted.org/packages/cc/92/cea9eb9d381ca57065a5eb4ec2ce7a291bd96c85ce742915c3c9ffc1069f/websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5", size = 163673, upload-time = "2024-09-21T17:33:51.056Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f1/279104fff239bfd04c12b1e58afea227d72fd1acf431e3eed3f6ac2c96d2/websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c", size = 158702, upload-time = "2024-09-21T17:33:52.584Z" }, - { url = "https://files.pythonhosted.org/packages/25/0b/b87370ff141375c41f7dd67941728e4b3682ebb45882591516c792a2ebee/websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d", size = 159146, upload-time = "2024-09-21T17:33:53.781Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499, upload-time = "2024-09-21T17:33:54.917Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737, upload-time = "2024-09-21T17:33:56.052Z" }, - { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095, upload-time = "2024-09-21T17:33:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701, upload-time = "2024-09-21T17:33:59.061Z" }, - { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654, upload-time = "2024-09-21T17:34:00.944Z" }, - { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192, upload-time = "2024-09-21T17:34:02.656Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a1/5ae6d0ef2e61e2b77b3b4678949a634756544186620a728799acdf5c3482/websockets-13.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b", size = 155433, upload-time = "2024-09-21T17:34:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/addd33f85600d210a445f817ff0d79d2b4d0eb6f3c95b9f35531ebf8f57c/websockets-13.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51", size = 155733, upload-time = "2024-09-21T17:34:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/f8ec74ac3b14a983289a1b42dc2c518a0e2030b486d0549d4f51ca11e7c9/websockets-13.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7", size = 157093, upload-time = "2024-09-21T17:34:06.398Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4c/aa5cc2f718ee4d797411202f332c8281f04c42d15f55b02f7713320f7a03/websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d", size = 156701, upload-time = "2024-09-21T17:34:07.582Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4b/7c5b2d0d0f0f1a54f27c60107cf1f201bee1f88c5508f87408b470d09a9c/websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027", size = 156648, upload-time = "2024-09-21T17:34:08.734Z" }, - { url = "https://files.pythonhosted.org/packages/f3/63/35f3fb073884a9fd1ce5413b2dcdf0d9198b03dac6274197111259cbde06/websockets-13.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978", size = 159188, upload-time = "2024-09-21T17:34:10.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/fd/e4bf9a7159dba6a16c59ae9e670e3e8ad9dcb6791bc0599eb86de32d50a9/websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e", size = 155499, upload-time = "2024-09-21T17:34:11.3Z" }, - { url = "https://files.pythonhosted.org/packages/74/42/d48ede93cfe0c343f3b552af08efc60778d234989227b16882eed1b8b189/websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09", size = 155731, upload-time = "2024-09-21T17:34:13.151Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f2/2ef6bff1c90a43b80622a17c0852b48c09d3954ab169266ad7b15e17cdcb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842", size = 157093, upload-time = "2024-09-21T17:34:14.52Z" }, - { url = "https://files.pythonhosted.org/packages/d1/14/6f20bbaeeb350f155edf599aad949c554216f90e5d4ae7373d1f2e5931fb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb", size = 156701, upload-time = "2024-09-21T17:34:15.692Z" }, - { url = "https://files.pythonhosted.org/packages/c7/86/38279dfefecd035e22b79c38722d4f87c4b6196f1556b7a631d0a3095ca7/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20", size = 156649, upload-time = "2024-09-21T17:34:17.335Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c5/12c6859a2eaa8c53f59a647617a27f1835a226cd7106c601067c53251d98/websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678", size = 159187, upload-time = "2024-09-21T17:34:18.538Z" }, - { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, -] - -[[package]] -name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, - { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, - { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424, upload-time = "2025-03-05T20:02:56.505Z" }, - { url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077, upload-time = "2025-03-05T20:02:58.37Z" }, - { url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324, upload-time = "2025-03-05T20:02:59.773Z" }, - { url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094, upload-time = "2025-03-05T20:03:01.827Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094, upload-time = "2025-03-05T20:03:03.123Z" }, - { url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397, upload-time = "2025-03-05T20:03:04.443Z" }, - { url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794, upload-time = "2025-03-05T20:03:06.708Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194, upload-time = "2025-03-05T20:03:08.844Z" }, - { url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164, upload-time = "2025-03-05T20:03:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381, upload-time = "2025-03-05T20:03:12.77Z" }, - { url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841, upload-time = "2025-03-05T20:03:14.367Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, - { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, - { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, - { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106, upload-time = "2025-03-05T20:03:29.404Z" }, - { url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339, upload-time = "2025-03-05T20:03:30.755Z" }, - { url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597, upload-time = "2025-03-05T20:03:32.247Z" }, - { url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205, upload-time = "2025-03-05T20:03:33.731Z" }, - { url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150, upload-time = "2025-03-05T20:03:35.757Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877, upload-time = "2025-03-05T20:03:37.199Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, -] - -[[package]] -name = "win32-setctime" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, -] From d11d09de74d193a6001567ea165e3051cf4b66b7 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Thu, 23 Oct 2025 13:10:58 -0500 Subject: [PATCH 2/4] Added fixes for Devtools and Agent Server connection. Fixed Dockerfile --- .../nodejs/src/lib/BrowserAgentServer.js | 65 +++++++- docker/Dockerfile | 83 ++++++++-- docker/README.md | 144 +++++++++++++----- docker/docker-compose.yml | 21 ++- 4 files changed, 255 insertions(+), 58 deletions(-) diff --git a/agent-server/nodejs/src/lib/BrowserAgentServer.js b/agent-server/nodejs/src/lib/BrowserAgentServer.js index f4a43b2abb..fd5a48de02 100644 --- a/agent-server/nodejs/src/lib/BrowserAgentServer.js +++ b/agent-server/nodejs/src/lib/BrowserAgentServer.js @@ -773,11 +773,66 @@ export class BrowserAgentServer extends EventEmitter { */ async getCDPBrowserEndpoint() { try { - const cdpUrl = `http://${CONFIG.cdp.host}:${CONFIG.cdp.port}/json/version`; - logger.info('Attempting to connect to CDP', { cdpUrl }); - const response = await fetch(cdpUrl); - const data = await response.json(); - return data.webSocketDebuggerUrl; + const path = '/json/version'; + logger.info('Attempting to connect to CDP', { + host: CONFIG.cdp.host, + port: CONFIG.cdp.port, + path + }); + + // When connecting via host.docker.internal, we need to set Host header to localhost + // because Chrome only accepts CDP requests with localhost/127.0.0.1 in the Host header + const headers = {}; + if (CONFIG.cdp.host === 'host.docker.internal') { + headers['Host'] = `localhost:${CONFIG.cdp.port}`; + logger.info('Using Host header override for host.docker.internal', headers); + } + + const options = { + hostname: CONFIG.cdp.host, + port: CONFIG.cdp.port, + path: path, + method: 'GET', + headers: headers + }; + + const http = await import('http'); + + return new Promise((resolve, reject) => { + const req = http.default.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const jsonData = JSON.parse(data); + let wsUrl = jsonData.webSocketDebuggerUrl; + + // Replace localhost with host.docker.internal when running in Docker + if (CONFIG.cdp.host === 'host.docker.internal' && wsUrl) { + wsUrl = wsUrl.replace('ws://localhost:', 'ws://host.docker.internal:'); + wsUrl = wsUrl.replace('ws://127.0.0.1:', 'ws://host.docker.internal:'); + logger.info('Rewrote WebSocket URL for Docker', { original: jsonData.webSocketDebuggerUrl, rewritten: wsUrl }); + } + + resolve(wsUrl); + } catch (parseError) { + logger.error('Failed to parse CDP response', { error: parseError.message, data }); + reject(new Error('Failed to connect to Chrome DevTools Protocol')); + } + }); + }); + + req.on('error', (error) => { + logger.error('Failed to get CDP browser endpoint', { error: error.message }); + reject(new Error('Failed to connect to Chrome DevTools Protocol')); + }); + + req.end(); + }); } catch (error) { logger.error('Failed to get CDP browser endpoint', { error: error.message }); throw new Error('Failed to connect to Chrome DevTools Protocol'); diff --git a/docker/Dockerfile b/docker/Dockerfile index 2a6cb50340..2caba17628 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -47,25 +47,88 @@ RUN git remote add upstream https://github.com/BrowserOperator/browser-operator- RUN git fetch upstream RUN git checkout upstream/main -# Allow configurable automated mode -ARG AUTOMATED_MODE=false - -# Set build-time flags based on Docker arg -RUN if [ "$AUTOMATED_MODE" = "true" ]; then \ - sed -i 's/AUTOMATED_MODE: false/AUTOMATED_MODE: true/' \ - front_end/panels/ai_chat/core/BuildConfig.ts; \ - fi +# Force automated mode (matches parent Dockerfile.devtools approach - rock-solid) +RUN sed -i 's/AUTOMATED_MODE: false/AUTOMATED_MODE: true/' \ + front_end/panels/ai_chat/core/BuildConfig.ts || true # Build Browser Operator version with current changes RUN npm run build -# Production stage +# ============================================================================== +# Agent Server build stage +# ============================================================================== +FROM --platform=linux/amd64 node:18-alpine AS agent-server-builder + +WORKDIR /workspace + +# Copy agent-server from local directory +COPY agent-server/nodejs /workspace/agent-server + +WORKDIR /workspace/agent-server + +# Install dependencies +RUN npm install + +# ============================================================================== +# Production stage - DevTools + Agent Server +# ============================================================================== FROM --platform=linux/amd64 nginx:alpine +# Install Node.js (required for agent-server) +RUN apk add --no-cache nodejs npm bash + # Copy the built DevTools frontend COPY --from=builder /workspace/devtools/devtools-frontend/out/Default/gen/front_end /usr/share/nginx/html +# Copy agent-server from builder stage +COPY --from=agent-server-builder /workspace/agent-server /opt/agent-server + # Copy nginx config COPY docker/nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 8000 \ No newline at end of file +# Create startup script to run both nginx and agent-server +RUN cat > /usr/local/bin/start-services.sh <<'EOFSCRIPT' +#!/bin/bash +set -e + +# Start nginx in background +echo "Starting nginx..." +nginx -g "daemon off;" & +NGINX_PID=$! + +# Start agent-server in background +echo "Starting agent-server..." +cd /opt/agent-server +node start.js & +AGENT_PID=$! + +# Wait for both processes +echo "DevTools running on http://localhost:8000" +echo "Agent Server WebSocket on ws://localhost:8082" +echo "Agent Server HTTP API on http://localhost:8080" +echo "Press Ctrl+C to stop..." + +# Trap SIGTERM and SIGINT +trap "kill $NGINX_PID $AGENT_PID 2>/dev/null || true; exit 0" SIGTERM SIGINT + +# Wait for either process to exit +wait -n $NGINX_PID $AGENT_PID + +# If one exits, kill the other +kill $NGINX_PID $AGENT_PID 2>/dev/null || true +EOFSCRIPT + +RUN chmod +x /usr/local/bin/start-services.sh + +# Environment variables for agent-server +ENV PORT=8082 +ENV API_PORT=8080 +ENV HOST=0.0.0.0 +ENV CDP_HOST=host.docker.internal +ENV CDP_PORT=9222 + +EXPOSE 8000 8080 8082 + +# Override the nginx entrypoint +ENTRYPOINT [] +CMD ["/bin/bash", "/usr/local/bin/start-services.sh"] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index ece2c08ebf..5bf1af6287 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,12 +1,13 @@ -# Docker Setup for Chrome DevTools Frontend +# Docker Setup for Browser Operator DevTools + Agent Server -This directory contains Docker configuration files for building and running the Chrome DevTools Frontend in a containerized environment. +This directory contains Docker configuration files for building and running the Browser Operator DevTools Frontend with integrated Agent Server in a containerized environment. ## Overview The Docker setup uses a multi-stage build process: -1. **Build Stage**: Compiles the DevTools frontend using the full development environment -2. **Production Stage**: Serves only the built files using Nginx (lightweight, ~50MB final image) +1. **DevTools Build Stage**: Compiles the DevTools frontend using the full development environment +2. **Agent Server Build Stage**: Installs Node.js dependencies for the agent server +3. **Production Stage**: Serves DevTools via Nginx + runs Agent Server (Node.js) in the same container ## Prerequisites @@ -22,11 +23,8 @@ The Docker setup uses a multi-stage build process: From the repository root directory: ```bash -# Build with automated mode (default - bypasses OAuth, auto-enables evaluation) -docker build -f docker/Dockerfile -t browser-operator-automated . - -# Build with normal mode (requires manual authentication) -docker build -f docker/Dockerfile --build-arg AUTOMATED_MODE=false -t browser-operator-manual . +# Build DevTools image (AUTOMATED_MODE is always enabled) +docker build -f docker/Dockerfile -t browser-operator-devtools . # Or use docker-compose (recommended) docker-compose -f docker/docker-compose.yml build @@ -35,67 +33,143 @@ docker-compose -f docker/docker-compose.yml build ### Running the Container ```bash -# Automated mode (no authentication required, evaluation auto-enabled) -docker run -d -p 8000:8000 --name browser-operator-automated browser-operator-automated - -# Manual mode (requires OAuth/API key setup) -docker run -d -p 8000:8000 --name browser-operator-manual browser-operator-manual +# Run DevTools container (AUTOMATED_MODE enabled by default) +docker run -d -p 8000:8000 --name browser-operator-devtools browser-operator-devtools # Or using docker-compose (recommended) docker-compose -f docker/docker-compose.yml up -d ``` -The DevTools will be available at: http://localhost:8000 +The services will be available at: +- **DevTools UI**: http://localhost:8000 +- **Agent Server HTTP API**: http://localhost:8080 +- **Agent Server WebSocket**: ws://localhost:8082 ### Accessing DevTools -Once the container is running, open Chrome or Chromium with: +Once the container is running, open Chrome or Chromium with remote debugging enabled: ```bash # macOS -/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --custom-devtools-frontend=http://localhost:8000/ +"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + --remote-debugging-port=9222 \ + --remote-allow-origins="*" \ + --auto-open-devtools-for-tabs \ + --user-data-dir=/tmp/chrome-debug-profile \ + --custom-devtools-frontend=http://localhost:8000/ # Linux -google-chrome --custom-devtools-frontend=http://localhost:8000/ +google-chrome \ + --remote-debugging-port=9222 \ + --remote-allow-origins="*" \ + --auto-open-devtools-for-tabs \ + --user-data-dir=/tmp/chrome-debug-profile \ + --custom-devtools-frontend=http://localhost:8000/ # Windows -chrome.exe --custom-devtools-frontend=http://localhost:8000/ +"C:\Program Files\Google\Chrome\Application\chrome.exe" \ + --remote-debugging-port=9222 \ + --remote-allow-origins="*" \ + --auto-open-devtools-for-tabs \ + --user-data-dir=C:\temp\chrome-debug-profile \ + --custom-devtools-frontend=http://localhost:8000/ ``` +**Important flags:** +- `--remote-debugging-port=9222` - Enables CDP for the Agent Server to connect +- `--remote-allow-origins="*"` - Allows CDP connections from Docker containers +- `--auto-open-devtools-for-tabs` - Automatically opens DevTools for new tabs (required for agent-server automation) +- `--user-data-dir=/tmp/chrome-debug-profile` - Uses a temporary profile to avoid conflicts +- `--custom-devtools-frontend=http://localhost:8000/` - Uses the Browser Operator DevTools + +**Note:** Make sure to completely quit Chrome before starting it with these flags. On macOS, use `Cmd+Q` or run `killall "Google Chrome"`. + ## File Structure ``` docker/ -├── Dockerfile # Multi-stage build configuration +├── Dockerfile # Multi-stage build (DevTools + Agent Server) ├── .dockerignore # Files to exclude from Docker context ├── nginx.conf # Nginx server configuration ├── docker-compose.yml # Docker Compose configuration └── README.md # This file + +../agent-server/ # Agent Server source code (included in build) ``` -## Automated Mode vs Manual Mode +## Automated Mode (Always Enabled) -### Automated Mode (Default) -- **Purpose**: Optimized for Docker/CI environments and automated workflows -- **Authentication**: Bypasses OAuth panel - no manual setup required -- **Evaluation**: Automatically enables evaluation mode for API wrapper connectivity -- **Use cases**: Production deployments, CI/CD, headless automation, API integration +This Docker image is built with **AUTOMATED_MODE** always enabled for seamless deployment: -### Manual Mode -- **Purpose**: Standard interactive usage -- **Authentication**: Requires OAuth setup or API key configuration -- **Evaluation**: Manual enable/disable in settings -- **Use cases**: Development, interactive testing, manual usage +- **Authentication**: Bypasses OAuth panel - no manual setup required +- **Evaluation**: Automatically enables evaluation mode for agent-server connectivity +- **Use cases**: Production deployments, CI/CD, headless automation, API integration ```bash -# Example automated mode workflow -docker build -f docker/Dockerfile -t browser-operator-automated . -docker run -d -p 8000:8000 --name browser-operator browser-operator-automated +# Example workflow - ready to use immediately +docker build -f docker/Dockerfile -t browser-operator-devtools . +docker run -d -p 8000:8000 --name browser-operator browser-operator-devtools # Ready to use immediately - no authentication required! -# Evaluation server can connect automatically via WebSocket (ws://localhost:8080) +# Agent server can connect automatically via WebSocket (ws://localhost:8082) ``` +**Note**: This approach matches the parent repository's `Dockerfile.devtools` which has proven to work "rock-solid" in production. + +## Agent Server + +The container includes a fully functional Agent Server that provides: + +### WebSocket API (port 8082) +- JSON-RPC 2.0 bidirectional communication +- Browser agent lifecycle management +- Direct CDP integration + +### HTTP REST API (port 8080) +- `POST /v1/responses` - Send tasks to browser agents +- `POST /page/screenshot` - Capture screenshots via CDP +- `POST /page/content` - Get HTML/text content +- `GET /status` - Health check + +### Configuration + +The Agent Server runs with these default settings: +- **WebSocket Port**: 8082 +- **HTTP API Port**: 8080 +- **Host**: 0.0.0.0 (listens on all interfaces) +- **Authentication**: Disabled (automated mode) + +To customize, you can override environment variables: + +```bash +docker run -d -p 8000:8000 -p 8080:8080 -p 8082:8082 \ + -e EVAL_SERVER_WS_PORT=8082 \ + -e EVAL_SERVER_HTTP_PORT=8080 \ + -e EVAL_SERVER_HOST=0.0.0.0 \ + browser-operator-devtools +``` + +### Testing the Agent Server + +```bash +# Health check +curl http://localhost:8080/status + +# Send a task (requires browser with remote debugging) +curl -X POST http://localhost:8080/v1/responses \ + -H "Content-Type: application/json" \ + -d '{ + "input": "Navigate to google.com", + "url": "about:blank", + "wait_timeout": 5000, + "model": { + "main_model": {"provider": "openai", "model": "gpt-4", "api_key": "sk-..."} + } + }' +``` + +For more details on the Agent Server API, see `../agent-server/README.md`. + ## Advanced Usage ### Development Mode diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 55a144350f..43e9c1021d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,20 +5,25 @@ services: build: context: .. dockerfile: docker/Dockerfile - image: devtools-frontend:latest - container_name: devtools-frontend + image: browser-operator-devtools:latest + container_name: browser-operator-devtools ports: - - "8000:8000" + - "8000:8000" # DevTools nginx + - "8080:8080" # Agent Server HTTP API + - "8082:8082" # Agent Server WebSocket restart: unless-stopped - volumes: - # For development: mount the built files directly (optional) - # Uncomment the line below to use local files instead of container files - # - ../out/Default/gen/front_end:/usr/share/nginx/html:ro + # volumes: + # # For development: mount the built files directly (optional) + # # Uncomment the line below to use local files instead of container files + # # - ../out/Default/gen/front_end:/usr/share/nginx/html:ro environment: - NGINX_HOST=localhost - NGINX_PORT=8000 + # Agent Server CDP configuration + - CDP_HOST=host.docker.internal + - CDP_PORT=9222 healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/"] + test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:8000/ && wget --no-verbose --tries=1 --spider http://localhost:8080/"] interval: 30s timeout: 10s retries: 3 From 220aea180d02ea7ef88c5179e563a0cc4f643f33 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Fri, 24 Oct 2025 11:10:28 -0500 Subject: [PATCH 3/4] Added client config autogeneration. --- agent-server/nodejs/init-client.sh | 43 +++++++++++++++++++++++ agent-server/nodejs/src/client-manager.js | 9 +++-- docker/Dockerfile | 8 +++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100755 agent-server/nodejs/init-client.sh diff --git a/agent-server/nodejs/init-client.sh b/agent-server/nodejs/init-client.sh new file mode 100755 index 0000000000..46c07a7f32 --- /dev/null +++ b/agent-server/nodejs/init-client.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Auto-generate client configuration for AUTOMATED_MODE +# This script creates a default client YAML file if none exist (excluding example-client.yaml) + +set -e + +CLIENTS_DIR="${CLIENTS_DIR:-/opt/agent-server/clients}" +CLIENT_NAME="${CLIENT_NAME:-DevTools Client}" + +# Ensure clients directory exists +mkdir -p "$CLIENTS_DIR" + +# Count actual client files (excluding example-client.yaml) +CLIENT_COUNT=$(find "$CLIENTS_DIR" -name "*.yaml" -o -name "*.yml" 2>/dev/null | grep -v "example-client" | wc -l) + +if [ "$CLIENT_COUNT" -eq 0 ]; then + echo "🔧 No client configurations found. Generating default client..." + + # Generate a random UUID for the client + if command -v uuidgen &> /dev/null; then + CLIENT_ID=$(uuidgen | tr '[:upper:]' '[:lower:]') + else + # Fallback UUID generation using /proc/sys/kernel/random/uuid + CLIENT_ID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 32 | head -n 1 | sed 's/\(.\{8\}\)\(.\{4\}\)\(.\{4\}\)\(.\{4\}\)\(.\{12\}\)/\1-\2-\3-\4-\5/')") + fi + + # Create client YAML configuration + cat > "$CLIENTS_DIR/$CLIENT_ID.yaml" </dev/null || date) + +client: + id: $CLIENT_ID + name: $CLIENT_NAME + secret_key: null + description: Auto-generated client for AUTOMATED_MODE +EOF + + echo "✅ Created client configuration: $CLIENT_ID" + echo " Path: $CLIENTS_DIR/$CLIENT_ID.yaml" +else + echo "â„šī¸ Found $CLIENT_COUNT existing client configuration(s). Skipping auto-generation." +fi diff --git a/agent-server/nodejs/src/client-manager.js b/agent-server/nodejs/src/client-manager.js index 3c101c6b94..77c9295215 100644 --- a/agent-server/nodejs/src/client-manager.js +++ b/agent-server/nodejs/src/client-manager.js @@ -29,8 +29,13 @@ class ClientManager { // Only load base client YAML files, not composite ones with tab IDs const clientId = path.basename(f, path.extname(f)); return !clientId.includes(':'); + }) + .filter(f => { + // Skip example client template file + const filename = path.basename(f, path.extname(f)); + return filename !== 'example-client'; }); - + for (const file of files) { const clientId = path.basename(file, path.extname(file)); try { @@ -39,7 +44,7 @@ class ClientManager { logger.error(`Failed to load client ${clientId}:`, error); } } - + logger.info(`Loaded ${this.clients.size} clients`); } catch (error) { logger.error('Failed to load clients:', error); diff --git a/docker/Dockerfile b/docker/Dockerfile index 2caba17628..fe5b1a90d2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -83,6 +83,10 @@ COPY --from=builder /workspace/devtools/devtools-frontend/out/Default/gen/front_ # Copy agent-server from builder stage COPY --from=agent-server-builder /workspace/agent-server /opt/agent-server +# Copy client initialization script +COPY agent-server/nodejs/init-client.sh /opt/agent-server/init-client.sh +RUN chmod +x /opt/agent-server/init-client.sh + # Copy nginx config COPY docker/nginx.conf /etc/nginx/conf.d/default.conf @@ -91,6 +95,10 @@ RUN cat > /usr/local/bin/start-services.sh <<'EOFSCRIPT' #!/bin/bash set -e +# Initialize client configuration if needed +echo "Initializing client configuration..." +/opt/agent-server/init-client.sh + # Start nginx in background echo "Starting nginx..." nginx -g "daemon off;" & From 70dd5b71e931dd4877954e1c3239fb75a9b49388 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Fri, 24 Oct 2025 13:10:49 -0500 Subject: [PATCH 4/4] Cleanup client ID generation --- agent-server/nodejs/init-client.sh | 43 ----------------------- agent-server/nodejs/src/client-manager.js | 24 +++++++++++++ docker/Dockerfile | 8 ----- 3 files changed, 24 insertions(+), 51 deletions(-) delete mode 100755 agent-server/nodejs/init-client.sh diff --git a/agent-server/nodejs/init-client.sh b/agent-server/nodejs/init-client.sh deleted file mode 100755 index 46c07a7f32..0000000000 --- a/agent-server/nodejs/init-client.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# Auto-generate client configuration for AUTOMATED_MODE -# This script creates a default client YAML file if none exist (excluding example-client.yaml) - -set -e - -CLIENTS_DIR="${CLIENTS_DIR:-/opt/agent-server/clients}" -CLIENT_NAME="${CLIENT_NAME:-DevTools Client}" - -# Ensure clients directory exists -mkdir -p "$CLIENTS_DIR" - -# Count actual client files (excluding example-client.yaml) -CLIENT_COUNT=$(find "$CLIENTS_DIR" -name "*.yaml" -o -name "*.yml" 2>/dev/null | grep -v "example-client" | wc -l) - -if [ "$CLIENT_COUNT" -eq 0 ]; then - echo "🔧 No client configurations found. Generating default client..." - - # Generate a random UUID for the client - if command -v uuidgen &> /dev/null; then - CLIENT_ID=$(uuidgen | tr '[:upper:]' '[:lower:]') - else - # Fallback UUID generation using /proc/sys/kernel/random/uuid - CLIENT_ID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 32 | head -n 1 | sed 's/\(.\{8\}\)\(.\{4\}\)\(.\{4\}\)\(.\{4\}\)\(.\{12\}\)/\1-\2-\3-\4-\5/')") - fi - - # Create client YAML configuration - cat > "$CLIENTS_DIR/$CLIENT_ID.yaml" </dev/null || date) - -client: - id: $CLIENT_ID - name: $CLIENT_NAME - secret_key: null - description: Auto-generated client for AUTOMATED_MODE -EOF - - echo "✅ Created client configuration: $CLIENT_ID" - echo " Path: $CLIENTS_DIR/$CLIENT_ID.yaml" -else - echo "â„šī¸ Found $CLIENT_COUNT existing client configuration(s). Skipping auto-generation." -fi diff --git a/agent-server/nodejs/src/client-manager.js b/agent-server/nodejs/src/client-manager.js index 77c9295215..b4999f8722 100644 --- a/agent-server/nodejs/src/client-manager.js +++ b/agent-server/nodejs/src/client-manager.js @@ -16,6 +16,11 @@ class ClientManager { } this.loadAllClients(); + + // Auto-generate default client if no clients exist (for AUTOMATED_MODE) + if (this.clients.size === 0) { + this.autoGenerateDefaultClient(); + } } /** @@ -150,6 +155,25 @@ class ClientManager { return { clientId, yamlPath }; } + /** + * Auto-generate a default client configuration (for AUTOMATED_MODE) + */ + autoGenerateDefaultClient() { + logger.info('No client configurations found. Auto-generating default client for AUTOMATED_MODE...'); + + const clientId = uuidv4(); + const clientName = 'DevTools Client'; + + try { + const result = this.createClientWithId(clientId, clientName, null); + logger.info(`✅ Auto-generated client configuration: ${clientId}`); + return result; + } catch (error) { + logger.error('Failed to auto-generate default client:', error); + throw error; + } + } + /** * Reload a specific client's configuration */ diff --git a/docker/Dockerfile b/docker/Dockerfile index fe5b1a90d2..2caba17628 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -83,10 +83,6 @@ COPY --from=builder /workspace/devtools/devtools-frontend/out/Default/gen/front_ # Copy agent-server from builder stage COPY --from=agent-server-builder /workspace/agent-server /opt/agent-server -# Copy client initialization script -COPY agent-server/nodejs/init-client.sh /opt/agent-server/init-client.sh -RUN chmod +x /opt/agent-server/init-client.sh - # Copy nginx config COPY docker/nginx.conf /etc/nginx/conf.d/default.conf @@ -95,10 +91,6 @@ RUN cat > /usr/local/bin/start-services.sh <<'EOFSCRIPT' #!/bin/bash set -e -# Initialize client configuration if needed -echo "Initializing client configuration..." -/opt/agent-server/init-client.sh - # Start nginx in background echo "Starting nginx..." nginx -g "daemon off;" &