Skip to content

Commit 8b7aaec

Browse files
committed
fix: find global promptfoo on Windows
1 parent c7f89af commit 8b7aaec

File tree

2 files changed

+204
-70
lines changed

2 files changed

+204
-70
lines changed

README.md

Lines changed: 154 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,176 @@
1-
# promptfoo - Python wrapper
2-
3-
[![PyPI version](https://badge.fury.io/py/promptfoo.svg)](https://pypi.org/project/promptfoo/)
4-
[![Python versions](https://img.shields.io/pypi/pyversions/promptfoo.svg)](https://pypi.org/project/promptfoo/)
5-
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6-
7-
Python wrapper for [promptfoo](https://www.promptfoo.dev) - the LLM testing, red teaming, and security evaluation framework.
8-
9-
## What is promptfoo?
10-
11-
Promptfoo is a TypeScript/Node.js tool for:
12-
13-
- **LLM Testing & Evaluation** - Compare prompts, models, and RAG systems
14-
- **Red Teaming** - Automated vulnerability testing and adversarial attacks
15-
- **Security Scanning** - Detect prompt injection, jailbreaks, and data leaks
16-
- **CI/CD Integration** - Add automated AI security checks to your pipeline
1+
# Promptfoo: LLM evals & red teaming
2+
3+
<p align="center">
4+
<a href="https://pypi.org/project/promptfoo/"><img src="https://badge.fury.io/py/promptfoo.svg" alt="PyPI version"></a>
5+
<a href="https://pypi.org/project/promptfoo/"><img src="https://img.shields.io/pypi/pyversions/promptfoo.svg" alt="Python versions"></a>
6+
<a href="https://npmjs.com/package/promptfoo"><img src="https://img.shields.io/npm/dm/promptfoo" alt="npm downloads"></a>
7+
<a href="https://github.com/promptfoo/promptfoo/blob/main/LICENSE"><img src="https://img.shields.io/github/license/promptfoo/promptfoo" alt="MIT license"></a>
8+
<a href="https://discord.gg/promptfoo"><img src="https://github.com/user-attachments/assets/2092591a-ccc5-42a7-aeb6-24a2808950fd" alt="Discord"></a>
9+
</p>
10+
11+
---
12+
13+
> **📦 About this Python package**
14+
>
15+
> This is a lightweight wrapper that installs promptfoo via `pip`. It requires **Node.js 18+** and executes `npx promptfoo@latest` under the hood.
16+
>
17+
> **💡 If you have Node.js installed**, we recommend using `npx promptfoo@latest` directly for better performance:
18+
>
19+
> ```bash
20+
> npx promptfoo@latest init
21+
> npx promptfoo@latest eval
22+
> ```
23+
>
24+
> See the [main project](https://github.com/promptfoo/promptfoo) for the official npm package.
25+
>
26+
> **🐍 Use this pip wrapper when you:**
27+
>
28+
> - Need to install via `pip` for Python-only CI/CD environments
29+
> - Want to manage promptfoo with poetry/pipenv/pip alongside Python dependencies
30+
> - Work in environments where pip packages are easier to approve than npm
31+
32+
---
33+
34+
<p align="center">
35+
<code>promptfoo</code> is a developer-friendly local tool for testing LLM applications. Stop the trial-and-error approach - start shipping secure, reliable AI apps.
36+
</p>
37+
38+
<p align="center">
39+
<a href="https://www.promptfoo.dev">Website</a> ·
40+
<a href="https://www.promptfoo.dev/docs/getting-started/">Getting Started</a> ·
41+
<a href="https://www.promptfoo.dev/docs/red-team/">Red Teaming</a> ·
42+
<a href="https://www.promptfoo.dev/docs/">Documentation</a> ·
43+
<a href="https://discord.gg/promptfoo">Discord</a>
44+
</p>
1745
1846
## Installation
1947
2048
### Requirements
2149
2250
- **Python 3.9+** (for this wrapper)
23-
- **Node.js 18+** (to run the actual promptfoo CLI)
51+
- **Node.js 18+** (required to run promptfoo)
2452
2553
### Install from PyPI
2654
2755
```bash
2856
pip install promptfoo
2957
```
3058
31-
This Python package is a lightweight wrapper that calls the official promptfoo CLI via `npx`.
59+
### Alternative: Use npx (Recommended)
3260

33-
### Verify Installation
61+
If you have Node.js installed, you can skip the wrapper and use npx directly:
3462

3563
```bash
36-
# Check that Node.js is installed
37-
node --version
38-
39-
# Run promptfoo
40-
promptfoo --version
64+
npx promptfoo@latest init
65+
npx promptfoo@latest eval
4166
```
4267

68+
This is faster and gives you direct access to the latest version.
69+
4370
## Quick Start
4471

4572
```bash
46-
# Initialize a new project
73+
# Install
74+
pip install promptfoo
75+
76+
# Initialize project
4777
promptfoo init
4878

49-
# Run an evaluation
79+
# Run your first evaluation
5080
promptfoo eval
81+
```
5182

52-
# Start red teaming
53-
promptfoo redteam run
83+
See [Getting Started](https://www.promptfoo.dev/docs/getting-started/) (evals) or [Red Teaming](https://www.promptfoo.dev/docs/red-team/) (vulnerability scanning) for more.
5484

55-
# View results in the web UI
56-
promptfoo view
57-
```
85+
## What can you do with Promptfoo?
5886

59-
## Usage
87+
- **Test your prompts and models** with [automated evaluations](https://www.promptfoo.dev/docs/getting-started/)
88+
- **Secure your LLM apps** with [red teaming](https://www.promptfoo.dev/docs/red-team/) and vulnerability scanning
89+
- **Compare models** side-by-side (OpenAI, Anthropic, Azure, Bedrock, Ollama, and [more](https://www.promptfoo.dev/docs/providers/))
90+
- **Automate checks** in [CI/CD](https://www.promptfoo.dev/docs/integrations/ci-cd/)
91+
- **Review pull requests** for LLM-related security and compliance issues with [code scanning](https://www.promptfoo.dev/docs/code-scanning/)
92+
- **Share results** with your team
6093

61-
The `promptfoo` command behaves identically to the official Node.js CLI. All arguments are passed through:
94+
Here's what it looks like in action:
6295

63-
```bash
64-
# Get help
65-
promptfoo --help
96+
![prompt evaluation matrix - web viewer](https://www.promptfoo.dev/img/claude-vs-gpt-example@2x.png)
6697

67-
# Run tests
68-
promptfoo eval
98+
It works on the command line too:
6999

70-
# Generate red team attacks
71-
promptfoo redteam generate
100+
![prompt evaluation matrix - command line](https://github.com/promptfoo/promptfoo/assets/310310/480e1114-d049-40b9-bd5f-f81c15060284)
72101

73-
# Run vulnerability scans
74-
promptfoo redteam run
102+
It also can generate [security vulnerability reports](https://www.promptfoo.dev/docs/red-team/):
75103

76-
# View results
77-
promptfoo view
104+
![gen ai red team](https://www.promptfoo.dev/img/riskreport-1@2x.png)
78105

79-
# Export results
80-
promptfoo export --format json --output results.json
81-
```
106+
## Why Promptfoo?
82107

83-
## How It Works
108+
- 🚀 **Developer-first**: Fast, with features like live reload and caching
109+
- 🔒 **Private**: LLM evals run 100% locally - your prompts never leave your machine
110+
- 🔧 **Flexible**: Works with any LLM API or programming language
111+
- 💪 **Battle-tested**: Powers LLM apps serving 10M+ users in production
112+
- 📊 **Data-driven**: Make decisions based on metrics, not gut feel
113+
- 🤝 **Open source**: MIT licensed, with an active community
114+
115+
## How This Wrapper Works
84116

85117
This Python package is a thin wrapper that:
86118

87-
1. Checks if Node.js and npx are installed
88-
2. Executes `npx promptfoo@latest <your-args>`
119+
1. Checks if Node.js is installed
120+
2. Executes `npx promptfoo@latest <your-args>` (or uses globally installed promptfoo if available)
89121
3. Passes through all arguments and environment variables
90122
4. Returns the same exit code
91123

92-
The actual promptfoo logic runs via the TypeScript package from npm.
124+
The actual promptfoo logic runs via the official TypeScript package from npm. All features and commands work identically.
125+
126+
## Python-Specific Usage
127+
128+
### With pip
129+
130+
```bash
131+
pip install promptfoo
132+
promptfoo eval
133+
```
134+
135+
### With poetry
136+
137+
```bash
138+
poetry add --group dev promptfoo
139+
poetry run promptfoo eval
140+
```
141+
142+
### With requirements.txt
143+
144+
```bash
145+
echo "promptfoo>=0.2.0" >> requirements.txt
146+
pip install -r requirements.txt
147+
promptfoo eval
148+
```
93149

94-
## Why a Python Wrapper?
150+
### In CI/CD (GitHub Actions example)
95151

96-
Many Python developers prefer `pip install` over `npm install` for tools in their workflow. This wrapper allows you to:
152+
```yaml
153+
- name: Setup Node.js
154+
uses: actions/setup-node@v4
155+
with:
156+
node-version: "20"
97157

98-
- Install promptfoo alongside your Python dependencies
99-
- Use it in Python-based CI/CD pipelines
100-
- Manage it with standard Python tooling (pip, poetry, pipenv, etc.)
158+
- name: Install promptfoo
159+
run: pip install promptfoo
101160

102-
## Documentation
161+
- name: Run red team tests
162+
run: promptfoo redteam run
163+
```
164+
165+
## Learn More
103166
104-
- **Website**: https://www.promptfoo.dev
105-
- **Docs**: https://www.promptfoo.dev/docs
106-
- **GitHub**: https://github.com/promptfoo/promptfoo
107-
- **Discord**: https://discord.gg/promptfoo
167+
- 📚 [Full Documentation](https://www.promptfoo.dev/docs/intro/)
168+
- 🔐 [Red Teaming Guide](https://www.promptfoo.dev/docs/red-team/)
169+
- 🎯 [Getting Started](https://www.promptfoo.dev/docs/getting-started/)
170+
- 💻 [CLI Usage](https://www.promptfoo.dev/docs/usage/command-line/)
171+
- 📦 [Main Project (npm)](https://github.com/promptfoo/promptfoo)
172+
- 🤖 [Supported Models](https://www.promptfoo.dev/docs/providers/)
173+
- 🔬 [Code Scanning Guide](https://www.promptfoo.dev/docs/code-scanning/)
108174
109175
## Troubleshooting
110176
@@ -119,20 +185,43 @@ The wrapper needs Node.js to run. Install it:
119185

120186
### Slow First Run
121187

122-
The first time you run `promptfoo`, npx will download the latest version from npm. Subsequent runs are fast.
188+
The first time you run `promptfoo`, npx downloads the latest version from npm (typically ~50MB). Subsequent runs use the cached version and are fast.
189+
190+
To speed this up, install promptfoo globally:
191+
192+
```bash
193+
npm install -g promptfoo
194+
```
195+
196+
The Python wrapper will automatically use the global installation when available.
123197

124198
### Version Pinning
125199

126-
By default, this wrapper uses `npx promptfoo@latest`. To pin a specific version, set the `PROMPTFOO_VERSION` environment variable:
200+
By default, this wrapper uses `npx promptfoo@latest`. To pin a specific version:
127201

128202
```bash
129203
export PROMPTFOO_VERSION=0.95.0
130204
promptfoo --version
131205
```
132206

133-
## Development
207+
Or install a specific version globally:
208+
209+
```bash
210+
npm install -g promptfoo@0.95.0
211+
```
212+
213+
## Contributing
214+
215+
We welcome contributions! Check out our [contributing guide](https://www.promptfoo.dev/docs/contributing/) to get started.
216+
217+
Join our [Discord community](https://discord.gg/promptfoo) for help and discussion.
218+
219+
**For wrapper-specific issues**: Report them in this repository
220+
**For promptfoo features/bugs**: Report in the [main project](https://github.com/promptfoo/promptfoo)
134221

135-
This is a minimal wrapper - the actual promptfoo source code lives in the main TypeScript repository.
222+
<a href="https://github.com/promptfoo/promptfoo/graphs/contributors">
223+
<img src="https://contrib.rocks/image?repo=promptfoo/promptfoo" />
224+
</a>
136225

137226
## License
138227

src/promptfoo/cli.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ def _normalize_path(path: str) -> str:
4242
return os.path.normcase(os.path.abspath(path))
4343

4444

45+
def _strip_quotes(path: str) -> str:
46+
if len(path) >= 2 and path[0] == path[-1] and path[0] in ('"', "'"):
47+
return path[1:-1]
48+
return path
49+
50+
51+
def _split_path(path_value: str) -> list[str]:
52+
entries = []
53+
for entry in path_value.split(os.pathsep):
54+
entry = _strip_quotes(entry.strip())
55+
if entry:
56+
entries.append(entry)
57+
return entries
58+
59+
4560
def _resolve_argv0() -> Optional[str]:
4661
if not sys.argv:
4762
return None
@@ -56,21 +71,51 @@ def _resolve_argv0() -> Optional[str]:
5671
return None
5772

5873

74+
def _find_windows_promptfoo() -> Optional[str]:
75+
candidates = []
76+
for key in ("NPM_CONFIG_PREFIX", "npm_config_prefix"):
77+
prefix = os.environ.get(key)
78+
if prefix:
79+
candidates.append(prefix)
80+
appdata = os.environ.get("APPDATA")
81+
if appdata:
82+
candidates.append(os.path.join(appdata, "npm"))
83+
localappdata = os.environ.get("LOCALAPPDATA")
84+
if localappdata:
85+
candidates.append(os.path.join(localappdata, "npm"))
86+
for env_key in ("ProgramFiles", "ProgramFiles(x86)"):
87+
program_files = os.environ.get(env_key)
88+
if program_files:
89+
candidates.append(os.path.join(program_files, "nodejs"))
90+
for base in candidates:
91+
for name in ("promptfoo.cmd", "promptfoo.exe"):
92+
candidate = os.path.join(base, name)
93+
if os.path.isfile(candidate):
94+
return candidate
95+
return None
96+
97+
5998
def _find_external_promptfoo() -> Optional[str]:
6099
promptfoo_path = shutil.which("promptfoo")
61100
if not promptfoo_path:
101+
if os.name == "nt":
102+
return _find_windows_promptfoo()
62103
return None
63104
argv0_path = _resolve_argv0()
64105
if argv0_path and _normalize_path(promptfoo_path) == argv0_path:
65106
wrapper_dir = _normalize_path(os.path.dirname(promptfoo_path))
66107
path_entries = [
67108
entry
68-
for entry in os.environ.get("PATH", "").split(os.pathsep)
69-
if entry and _normalize_path(entry) != wrapper_dir
109+
for entry in _split_path(os.environ.get("PATH", ""))
110+
if _normalize_path(entry) != wrapper_dir
70111
]
71-
if not path_entries:
72-
return None
73-
return shutil.which("promptfoo", path=os.pathsep.join(path_entries))
112+
if path_entries:
113+
candidate = shutil.which("promptfoo", path=os.pathsep.join(path_entries))
114+
if candidate:
115+
return candidate
116+
if os.name == "nt":
117+
return _find_windows_promptfoo()
118+
return None
74119
return promptfoo_path
75120

76121

0 commit comments

Comments
 (0)