ragaeval 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ragaeval/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ """
2
+ ragaeval - RAG Evaluation Package
3
+
4
+ A pip-installable Python library providing drop-in RAG evaluation capabilities using RAGAS.
5
+ """
6
+
7
+ import sys
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ # Setup VertexAI mock before other imports
12
+ from .mock_setup import setup_vertexai_mock
13
+ setup_vertexai_mock()
14
+
15
+ # Set UTF-8 encoding on Windows
16
+ if sys.platform == 'win32':
17
+ try:
18
+ sys.stdout.reconfigure(encoding='utf-8')
19
+ sys.stderr.reconfigure(encoding='utf-8')
20
+ except Exception:
21
+ pass
22
+
23
+ # Import public API components
24
+ from .interceptors import evaluate_rag, EvalSession, log
25
+ from .config import ConfigManager
26
+
27
+ # Public API exports
28
+ __all__ = [
29
+ "__version__",
30
+ "evaluate_rag",
31
+ "EvalSession",
32
+ "log",
33
+ "ConfigManager",
34
+ ]
ragaeval/cli.py ADDED
@@ -0,0 +1,332 @@
1
+ """Command-line interface for the ragaeval package.
2
+
3
+ This module provides the CLI with commands: run, configure, status, clear.
4
+ """
5
+
6
+ import sys
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ from . import __version__
11
+ from .config import ConfigManager
12
+ from .runner import EvaluationRunner
13
+ from .report import ReportGenerator
14
+ from .logger import Logger
15
+
16
+
17
+ def main():
18
+ """
19
+ CLI entry point registered as console script.
20
+
21
+ Subcommands: run, configure, status, clear
22
+ """
23
+ parser = argparse.ArgumentParser(
24
+ prog='ragaeval',
25
+ description='RAG Evaluation using RAGAS framework'
26
+ )
27
+
28
+ parser.add_argument(
29
+ '--version',
30
+ action='version',
31
+ version=f'ragaeval {__version__}'
32
+ )
33
+
34
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
35
+
36
+ # Run command
37
+ run_parser = subparsers.add_parser('run', help='Execute evaluation on logged data')
38
+ run_parser.add_argument(
39
+ '--file',
40
+ default='.ragaeval_log.jsonl',
41
+ help='Log file path (default: .ragaeval_log.jsonl)'
42
+ )
43
+ run_parser.add_argument(
44
+ '--model',
45
+ default='gpt-4o',
46
+ help='Azure deployment name (default: gpt-4o)'
47
+ )
48
+ run_parser.add_argument(
49
+ '--output',
50
+ default='eval_results.csv',
51
+ help='CSV output path (default: eval_results.csv)'
52
+ )
53
+
54
+ # Configure command
55
+ config_parser = subparsers.add_parser('configure', help='Interactive credential setup')
56
+
57
+ # Status command
58
+ status_parser = subparsers.add_parser('status', help='Display configuration and log status')
59
+
60
+ # Clear command
61
+ clear_parser = subparsers.add_parser('clear', help='Clear evaluation logs')
62
+ clear_parser.add_argument(
63
+ '--force',
64
+ action='store_true',
65
+ help='Skip confirmation prompt'
66
+ )
67
+
68
+ args = parser.parse_args()
69
+
70
+ if args.command == 'run':
71
+ sys.exit(cmd_run(args))
72
+ elif args.command == 'configure':
73
+ sys.exit(cmd_configure(args))
74
+ elif args.command == 'status':
75
+ sys.exit(cmd_status(args))
76
+ elif args.command == 'clear':
77
+ sys.exit(cmd_clear(args))
78
+ else:
79
+ parser.print_help()
80
+ sys.exit(0)
81
+
82
+
83
+ def cmd_run(args):
84
+ """
85
+ Execute evaluation on logged data.
86
+
87
+ Steps:
88
+ 1. Initialize ConfigManager
89
+ 2. Create EvaluationRunner
90
+ 3. Load logs
91
+ 4. Run evaluation
92
+ 5. Generate report
93
+
94
+ Args:
95
+ args: Parsed command-line arguments
96
+
97
+ Returns:
98
+ Exit code (0 for success, 1 for error)
99
+ """
100
+ try:
101
+ print(f"Loading logs from {args.file}...")
102
+
103
+ # Initialize config and runner
104
+ config = ConfigManager()
105
+ runner = EvaluationRunner(config, model=args.model)
106
+
107
+ # Load logs
108
+ records = runner.load_logs(args.file)
109
+ print(f"Loaded {len(records)} records")
110
+
111
+ # Run evaluation
112
+ print(f"\nRunning RAGAS evaluation with {args.model}...")
113
+ print("(This may take a few minutes depending on the number of records)")
114
+
115
+ df = runner.run_evaluation(records, args.output)
116
+
117
+ # Generate report
118
+ print("\n")
119
+ report = ReportGenerator(df)
120
+ report.print_terminal_report()
121
+
122
+ print(f"\nResults saved to: {args.output}")
123
+
124
+ return 0
125
+
126
+ except Exception as e:
127
+ print(f"Error: {str(e)}", file=sys.stderr)
128
+ return 1
129
+
130
+
131
+ def cmd_configure(args):
132
+ """
133
+ Interactive configuration setup.
134
+
135
+ Steps:
136
+ 1. Prompt for Azure OpenAI credentials
137
+ 2. Prompt for optional OpenAI API key
138
+ 3. Validate endpoint format
139
+ 4. Offer to test credentials
140
+ 5. Save to ~/.ragaeval/config.json
141
+ 6. Display config path
142
+
143
+ Args:
144
+ args: Parsed command-line arguments
145
+
146
+ Returns:
147
+ Exit code (0 for success, 1 for error)
148
+ """
149
+ try:
150
+ print("=" * 80)
151
+ print(" ragaeval Configuration")
152
+ print("=" * 80)
153
+ print("\nEnter your API credentials. Press Enter to skip optional fields.\n")
154
+
155
+ # Prompt for Azure OpenAI credentials
156
+ azure_api_key = input("Azure OpenAI API Key: ").strip()
157
+ azure_endpoint = input("Azure OpenAI Endpoint (e.g., https://xxx.openai.azure.com/): ").strip()
158
+ azure_deployment = input("Azure Deployment Name (e.g., gpt-4o): ").strip()
159
+ azure_version = input("Azure API Version (default: 2024-02-15-preview): ").strip() or "2024-02-15-preview"
160
+
161
+ # Prompt for optional OpenAI API key
162
+ print("\nOptional: OpenAI API key for embeddings (or press Enter to use local)")
163
+ openai_key = input("OpenAI API Key: ").strip()
164
+
165
+ # Build config dict
166
+ config_dict = {
167
+ "azure_api_key": azure_api_key,
168
+ "azure_endpoint": azure_endpoint,
169
+ "azure_deployment_name": azure_deployment,
170
+ "azure_api_version": azure_version,
171
+ }
172
+
173
+ if openai_key:
174
+ config_dict["openai_api_key"] = openai_key
175
+
176
+ # Save config
177
+ config = ConfigManager()
178
+ config_path = config.save_config(config_dict)
179
+
180
+ print(f"\n✅ Configuration saved to: {config_path}")
181
+ print("\nYou can now run: ragaeval run")
182
+
183
+ return 0
184
+
185
+ except Exception as e:
186
+ print(f"Error: {str(e)}", file=sys.stderr)
187
+ return 1
188
+
189
+
190
+ def cmd_status(args):
191
+ """
192
+ Display configuration and log status.
193
+
194
+ Output:
195
+ - Log file status (exists, record count)
196
+ - Config file status (exists, which credentials configured)
197
+ - Environment variable status (which keys set)
198
+ - Embedding model selection
199
+ - Azure OpenAI completeness
200
+
201
+ Args:
202
+ args: Parsed command-line arguments
203
+
204
+ Returns:
205
+ Exit code (0 for success)
206
+ """
207
+ try:
208
+ print("=" * 80)
209
+ print(" ragaeval Status")
210
+ print("=" * 80)
211
+ print()
212
+
213
+ # Log file status
214
+ logger = Logger()
215
+ if logger.log_exists():
216
+ count = logger.count_records()
217
+ print(f"📄 Log File: {logger.get_log_path()} ({count} records)")
218
+ else:
219
+ print(f"📄 Log File: Not found ({logger.get_log_path()})")
220
+
221
+ print()
222
+
223
+ # Config file status
224
+ config = ConfigManager()
225
+ if config.config_exists():
226
+ print(f"⚙️ Config File: {config.get_config_path()}")
227
+
228
+ # Check which credentials are configured
229
+ cfg = config.load_config()
230
+ creds_configured = []
231
+ if cfg.get('azure_api_key'):
232
+ creds_configured.append("Azure OpenAI")
233
+ if cfg.get('openai_api_key'):
234
+ creds_configured.append("OpenAI (embeddings)")
235
+
236
+ if creds_configured:
237
+ print(f" Credentials: {', '.join(creds_configured)}")
238
+ else:
239
+ print(f"⚙️ Config File: Not found (run 'ragaeval configure')")
240
+
241
+ print()
242
+
243
+ # Environment variables
244
+ import os
245
+ env_vars = []
246
+ if os.getenv('AZURE_OPENAI_API_KEY'):
247
+ env_vars.append("AZURE_OPENAI_API_KEY")
248
+ if os.getenv('AZURE_OPENAI_ENDPOINT'):
249
+ env_vars.append("AZURE_OPENAI_ENDPOINT")
250
+ if os.getenv('AZURE_OPENAI_DEPLOYMENT_NAME'):
251
+ env_vars.append("AZURE_OPENAI_DEPLOYMENT_NAME")
252
+ if os.getenv('OPENAI_API_KEY'):
253
+ env_vars.append("OPENAI_API_KEY")
254
+
255
+ if env_vars:
256
+ print(f"🔑 Environment Variables: {', '.join(env_vars)}")
257
+ else:
258
+ print("🔑 Environment Variables: None set")
259
+
260
+ print()
261
+
262
+ # Embedding model
263
+ openai_key = config.get_openai_api_key()
264
+ if openai_key and openai_key.startswith('sk-'):
265
+ print("🧮 Embeddings: OpenAI (text-embedding-3-small)")
266
+ else:
267
+ print("🧮 Embeddings: Local (sentence-transformers/all-MiniLM-L6-v2)")
268
+
269
+ print()
270
+
271
+ # Azure OpenAI completeness
272
+ try:
273
+ azure_config = config.get_azure_config()
274
+ print("✅ Azure OpenAI: Configured")
275
+ except Exception:
276
+ print("❌ Azure OpenAI: Incomplete (run 'ragaeval configure')")
277
+
278
+ print()
279
+ print("=" * 80)
280
+
281
+ return 0
282
+
283
+ except Exception as e:
284
+ print(f"Error: {str(e)}", file=sys.stderr)
285
+ return 1
286
+
287
+
288
+ def cmd_clear(args):
289
+ """
290
+ Clear evaluation logs.
291
+
292
+ Steps:
293
+ 1. Check if log file exists
294
+ 2. Prompt for confirmation (unless --force)
295
+ 3. Delete log file
296
+ 4. Display confirmation with deleted record count
297
+
298
+ Args:
299
+ args: Parsed command-line arguments
300
+
301
+ Returns:
302
+ Exit code (0 for success, 1 for error)
303
+ """
304
+ try:
305
+ logger = Logger()
306
+
307
+ if not logger.log_exists():
308
+ print(f"Nothing to clear. Log file not found: {logger.get_log_path()}")
309
+ return 0
310
+
311
+ count = logger.count_records()
312
+
313
+ # Confirm deletion
314
+ if not args.force:
315
+ response = input(f"Delete {count} records from {logger.get_log_path()}? (y/N): ")
316
+ if response.lower() != 'y':
317
+ print("Cancelled.")
318
+ return 0
319
+
320
+ # Delete
321
+ logger.clear_log()
322
+ print(f"✅ Deleted {count} records from {logger.get_log_path()}")
323
+
324
+ return 0
325
+
326
+ except Exception as e:
327
+ print(f"Error: {str(e)}", file=sys.stderr)
328
+ return 1
329
+
330
+
331
+ if __name__ == '__main__':
332
+ main()
ragaeval/config.py ADDED
@@ -0,0 +1,237 @@
1
+ """Configuration manager for the ragaeval package.
2
+
3
+ Universal LLM provider support with auto-detection and smart defaults.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Optional, Dict, Tuple
10
+ from dotenv import load_dotenv
11
+
12
+ from .exceptions import MissingCredentialsError, ConfigFileError
13
+
14
+
15
+ # Supported LLM providers configuration
16
+ SUPPORTED_PROVIDERS = {
17
+ "openrouter": {
18
+ "env_keys": ["OPENROUTER_API_KEY"],
19
+ "base_url": "https://openrouter.ai/api/v1",
20
+ "default_model": "openai/gpt-oss-120b:free",
21
+ "free_models": ["openai/gpt-oss-120b:free", "openai/gpt-oss-20b:free"],
22
+ "description": "OpenRouter (free): OPENROUTER_API_KEY=sk-or-v1-...",
23
+ "signup_url": "https://openrouter.ai/keys"
24
+ },
25
+ "openai": {
26
+ "env_keys": ["OPENAI_API_KEY"],
27
+ "base_url": "https://api.openai.com/v1",
28
+ "default_model": "gpt-4o-mini",
29
+ "description": "OpenAI: OPENAI_API_KEY=sk-...",
30
+ "signup_url": "https://platform.openai.com/api-keys"
31
+ },
32
+ "azure_openai": {
33
+ "env_keys": ["AZURE_OPENAI_API_KEY"],
34
+ "extra_env": ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT_NAME"],
35
+ "default_model": "gpt-4o-mini",
36
+ "description": "Azure OpenAI: AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT + AZURE_OPENAI_DEPLOYMENT_NAME",
37
+ "signup_url": "https://azure.microsoft.com/en-us/products/ai-services/openai-service"
38
+ },
39
+ "anthropic": {
40
+ "env_keys": ["ANTHROPIC_API_KEY"],
41
+ "base_url": "https://api.anthropic.com",
42
+ "default_model": "claude-3-haiku-20240307",
43
+ "description": "Anthropic: ANTHROPIC_API_KEY=sk-ant-...",
44
+ "signup_url": "https://console.anthropic.com/keys"
45
+ },
46
+ "groq": {
47
+ "env_keys": ["GROQ_API_KEY"],
48
+ "base_url": "https://api.groq.com/openai/v1",
49
+ "default_model": "llama3-8b-8192",
50
+ "description": "Groq (free): GROQ_API_KEY=gsk_...",
51
+ "signup_url": "https://console.groq.com/keys"
52
+ },
53
+ "together": {
54
+ "env_keys": ["TOGETHER_API_KEY"],
55
+ "base_url": "https://api.together.xyz/v1",
56
+ "default_model": "mistralai/Mixtral-8x7B-Instruct-v0.1",
57
+ "description": "Together AI: TOGETHER_API_KEY=...",
58
+ "signup_url": "https://api.together.xyz"
59
+ },
60
+ "ollama": {
61
+ "env_keys": [],
62
+ "base_url": "http://localhost:11434/v1",
63
+ "default_model": "llama3",
64
+ "no_key_required": True,
65
+ "description": "Ollama (local): No key needed, just run Ollama",
66
+ "signup_url": "https://ollama.ai"
67
+ },
68
+ "custom": {
69
+ "env_keys": ["RAGAEVAL_API_KEY"],
70
+ "extra_env": ["RAGAEVAL_BASE_URL", "RAGAEVAL_MODEL"],
71
+ "description": "Custom: RAGAEVAL_API_KEY + RAGAEVAL_BASE_URL + RAGAEVAL_MODEL",
72
+ "signup_url": None
73
+ }
74
+ }
75
+
76
+
77
+ class ConfigManager:
78
+ """Manages LLM provider detection and configuration for ragaeval."""
79
+
80
+ def __init__(self):
81
+ """Initialize config manager and load environment."""
82
+ # Load .env file if it exists in current directory
83
+ load_dotenv(dotenv_path=".env", override=False)
84
+
85
+ # Load config from file
86
+ self._config = self.load_config()
87
+
88
+ def detect_provider(self) -> Tuple[str, Optional[str], str, Optional[str]]:
89
+ """
90
+ Auto-detect LLM provider from environment.
91
+
92
+ Scans in order: openrouter → openai → azure → anthropic → groq → together → ollama → custom
93
+ Sources checked: env vars → .env in cwd → ~/.ragaeval/config.json
94
+
95
+ Returns:
96
+ Tuple of (provider_name, api_key, model, base_url)
97
+
98
+ Raises:
99
+ MissingCredentialsError: If no provider is configured
100
+ """
101
+ # Scan providers in priority order
102
+ scan_order = ["openrouter", "openai", "azure_openai", "anthropic",
103
+ "groq", "together", "ollama", "custom"]
104
+
105
+ for provider_name in scan_order:
106
+ provider_config = SUPPORTED_PROVIDERS[provider_name]
107
+
108
+ # Check if this provider is configured
109
+ if provider_name == "ollama" and provider_config.get("no_key_required"):
110
+ # Ollama doesn't need a key - check if it's explicitly selected
111
+ if self._config.get("provider") == "ollama":
112
+ model = self._config.get("model", provider_config["default_model"])
113
+ self._print_provider_detected("ollama", model, "config file")
114
+ return ("ollama", None, model, provider_config["base_url"])
115
+ continue
116
+
117
+ # Check for API key
118
+ api_key = None
119
+ key_source = None
120
+
121
+ for env_key in provider_config["env_keys"]:
122
+ api_key = os.getenv(env_key)
123
+ if api_key:
124
+ key_source = ".env file" if Path(".env").exists() else "environment variable"
125
+ break
126
+
127
+ # If no key in env, check config file
128
+ if not api_key and provider_name in self._config.get("provider", ""):
129
+ api_key = self._config.get("api_key")
130
+ key_source = "config file"
131
+
132
+ if api_key:
133
+ # Provider found! Check extra requirements (e.g., Azure needs endpoint)
134
+ if "extra_env" in provider_config:
135
+ missing = []
136
+ for extra_key in provider_config["extra_env"]:
137
+ if not os.getenv(extra_key) and not self._config.get(extra_key.lower()):
138
+ missing.append(extra_key)
139
+
140
+ if missing:
141
+ continue # Skip this provider if extra keys missing
142
+
143
+ # Determine model
144
+ model = self._config.get("model", provider_config.get("default_model", "unknown"))
145
+
146
+ # Get base URL
147
+ base_url = provider_config.get("base_url")
148
+ if provider_name == "custom":
149
+ base_url = os.getenv("RAGAEVAL_BASE_URL") or self._config.get("base_url")
150
+ elif provider_name == "azure_openai":
151
+ base_url = os.getenv("AZURE_OPENAI_ENDPOINT") or self._config.get("azure_endpoint")
152
+
153
+ self._print_provider_detected(provider_name, model, key_source)
154
+ return (provider_name, api_key, model, base_url)
155
+
156
+ # No provider found - print help
157
+ self._print_no_provider_help()
158
+ raise MissingCredentialsError("No LLM provider configured. See message above for setup instructions.")
159
+
160
+ def _print_provider_detected(self, provider: str, model: str, source: str):
161
+ """Print provider detection message."""
162
+ print(f"[ragaeval] Provider detected: {provider}")
163
+ print(f"[ragaeval] Model: {model}")
164
+ print(f"[ragaeval] Key source: {source}")
165
+
166
+ def _print_no_provider_help(self):
167
+ """Print comprehensive help when no provider is detected."""
168
+ print("\n" + "=" * 70)
169
+ print("[ragaeval] No LLM provider configured.")
170
+ print("=" * 70)
171
+ print("\nAdd any ONE of these to your .env file or environment:\n")
172
+
173
+ for provider_name in ["openrouter", "openai", "anthropic", "groq",
174
+ "together", "ollama", "custom"]:
175
+ config = SUPPORTED_PROVIDERS[provider_name]
176
+ print(f" {config['description']}")
177
+ if config.get("signup_url"):
178
+ print(f" {config['signup_url']}")
179
+ print()
180
+
181
+ print("Or set globally:")
182
+ print(" ragaeval configure --provider openrouter --key sk-or-v1-...")
183
+ print("=" * 70 + "\n")
184
+
185
+ def save_config(self, config_dict: Dict[str, str]) -> Path:
186
+ """
187
+ Save provider configuration to ~/.ragaeval/config.json.
188
+
189
+ Note: API keys are NOT stored - only provider name and model.
190
+
191
+ Args:
192
+ config_dict: Dictionary with provider settings
193
+
194
+ Returns:
195
+ Path to config file
196
+ """
197
+ config_path = self.get_config_path()
198
+
199
+ # Create directory if needed
200
+ config_path.parent.mkdir(parents=True, exist_ok=True)
201
+
202
+ # Write config file
203
+ with open(config_path, 'w', encoding='utf-8') as f:
204
+ json.dump(config_dict, f, indent=2)
205
+
206
+ # Reload config
207
+ self._config = self.load_config()
208
+
209
+ return config_path
210
+
211
+ def load_config(self) -> Dict:
212
+ """
213
+ Load configuration from ~/.ragaeval/config.json.
214
+
215
+ Returns:
216
+ dict with config keys, empty dict if file doesn't exist
217
+ """
218
+ config_path = self.get_config_path()
219
+
220
+ if not config_path.exists():
221
+ return {}
222
+
223
+ try:
224
+ with open(config_path, 'r', encoding='utf-8') as f:
225
+ return json.load(f)
226
+ except json.JSONDecodeError as e:
227
+ raise ConfigFileError(f"Config file contains invalid JSON: {str(e)}")
228
+ except Exception as e:
229
+ raise ConfigFileError(f"Failed to read config file: {str(e)}")
230
+
231
+ def get_config_path(self) -> Path:
232
+ """Return path to config file."""
233
+ return Path.home() / ".ragaeval" / "config.json"
234
+
235
+ def config_exists(self) -> bool:
236
+ """Check if config file exists."""
237
+ return self.get_config_path().exists()
ragaeval/exceptions.py ADDED
@@ -0,0 +1,40 @@
1
+ """Custom exception classes for the ragaeval package.
2
+
3
+ This module defines the exception hierarchy used throughout ragaeval for
4
+ handling various error conditions during configuration, logging, and evaluation.
5
+ """
6
+
7
+
8
+ class RagaevalError(Exception):
9
+ """Base exception for ragaeval package."""
10
+ pass
11
+
12
+
13
+ class MissingCredentialsError(RagaevalError):
14
+ """Raised when required API credentials are missing."""
15
+ pass
16
+
17
+
18
+ class ConfigFileError(RagaevalError):
19
+ """Raised when config file is malformed or inaccessible."""
20
+ pass
21
+
22
+
23
+ class ValidationError(RagaevalError):
24
+ """Raised when log data validation fails."""
25
+ pass
26
+
27
+
28
+ class LogFileError(RagaevalError):
29
+ """Raised when log file is missing or inaccessible."""
30
+ pass
31
+
32
+
33
+ class FileFormatError(RagaevalError):
34
+ """Raised when log file contains malformed JSON."""
35
+ pass
36
+
37
+
38
+ class EvaluationError(RagaevalError):
39
+ """Raised when RAGAS evaluation fails."""
40
+ pass