Files
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

140 lines
5.2 KiB
Python

"""Extension hooks for promptfoo.
This module provides functionality for handling extension hooks in promptfoo.
It allows for executing custom actions before and after test suites and
individual evaluations, as well as running setup and teardown commands.
IMPORTANT: The function signature is (hook_name, context) where:
- hook_name: 'beforeAll', 'beforeEach', 'afterEach', or 'afterAll'
- context: The hook-specific data (e.g., {'suite': ...} for beforeAll)
WARNING: The `counter` variable is NOT thread-safe. When running with concurrency > 1,
the counter may not accurately track test numbers. Consider using thread-safe counters
or per-test metadata for accurate tracking in concurrent scenarios.
"""
import logging
import os
from datetime import datetime
from typing import Optional
# Set up logging only if it hasn't been set up already
if not logging.getLogger().handlers:
current_dir = os.path.dirname(os.path.abspath(__file__))
log_filename = f"promptfoo_run_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
log_path = os.path.join(current_dir, log_filename)
logging.basicConfig(
filename=log_path,
level=logging.INFO,
format="%(asctime)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# WARNING: Not thread-safe with concurrency > 1
counter = 0
def extension_hook(hook_name: str, context: dict) -> Optional[dict]:
"""Handles different extension hooks for promptfoo.
This function is called at various points during the test execution process.
It logs information about the test suite and individual tests.
Args:
hook_name (str): The name of the hook being called.
One of: 'beforeAll', 'beforeEach', 'afterEach', 'afterAll'
context (dict): A dictionary containing contextual information for the hook.
The contents vary depending on the hook being called:
- beforeAll: {'suite': TestSuite}
- beforeEach: {'test': TestCase}
- afterEach: {'test': TestCase, 'result': EvaluateResult}
- afterAll: {'results': EvaluateResult[], 'suite': TestSuite, ...}
Returns:
context (Optional[dict]): The "beforeAll" and "beforeEach" hooks should return
the context object (potentially modified), while the "afterAll" and "afterEach"
hooks should not return anything.
Global Variables:
counter (int): Keeps track of the number of tests completed.
WARNING: Not thread-safe when running with concurrency > 1.
Logs:
Information about the test suite and individual tests, including setup,
completion, results, and token usage.
"""
global counter
if hook_name == "beforeAll":
suite = context.get("suite", {})
logging.info(
f"Setting up test suite: {suite.get('description') or 'Unnamed suite'}"
)
logging.info(f"Total prompts: {len(suite.get('prompts', []))}")
logging.info(f"Total providers: {len(suite.get('providers', []))}")
logging.info(f"Total tests: {len(suite.get('tests', []))}")
# Add an additional test case to the suite:
context["suite"]["tests"].append(
{
"vars": {
"body": "It's a beautiful day",
"language": "Spanish",
},
"assert": [{"type": "contains", "value": "Es un día hermoso."}],
}
)
# Add an additional default assertion to the suite:
context["suite"]["defaultTest"]["assert"].append({"type": "is-json"})
return context
elif hook_name == "beforeEach":
logging.info("Preparing test")
# All languages are now pirate:
context["test"]["vars"]["language"] = (
f"Pirate {context['test']['vars']['language']}"
)
return context
elif hook_name == "afterEach":
result = context.get("result", {})
result_str = ""
if result:
success = "Pass" if result.get("success") else "Fail"
score = result.get("score", 0)
result_str = f", Result: {success}, Score: {score}"
logging.info(f"Completed test {counter}{result_str}")
# Access sessionId if available (from multi-turn conversations or stateful tests)
session_id = result.get("metadata", {}).get("sessionId")
if session_id:
logging.info(f"Session ID: {session_id}")
session_ids = result.get("metadata", {}).get("sessionIds")
if session_ids:
logging.info(f"Session IDs: {session_ids}")
counter += 1
elif hook_name == "afterAll":
results = context.get("results", [])
logging.info("Test suite completed")
logging.info(f"Total tests run: {len(results)}")
successes = sum(1 for r in results if r.get("success"))
failures = sum(1 for r in results if not r.get("success"))
logging.info(f"Successes: {successes}")
logging.info(f"Failures: {failures}")
total_token_usage = sum(
r.get("response", {}).get("tokenUsage", {}).get("total", 0) for r in results
)
logging.info(f"Total token usage: {total_token_usage}")
logging.info("") # Add a blank line for readability between hooks
return None