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 +34 -0
- ragaeval/cli.py +332 -0
- ragaeval/config.py +237 -0
- ragaeval/exceptions.py +40 -0
- ragaeval/field_utils.py +77 -0
- ragaeval/interceptors.py +211 -0
- ragaeval/logger.py +227 -0
- ragaeval/mock_setup.py +51 -0
- ragaeval/report.py +176 -0
- ragaeval/runner.py +283 -0
- ragaeval-0.1.0.dist-info/METADATA +265 -0
- ragaeval-0.1.0.dist-info/RECORD +16 -0
- ragaeval-0.1.0.dist-info/WHEEL +5 -0
- ragaeval-0.1.0.dist-info/entry_points.txt +2 -0
- ragaeval-0.1.0.dist-info/licenses/LICENSE +21 -0
- ragaeval-0.1.0.dist-info/top_level.txt +1 -0
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
|