snowglobe 0.4.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.
- snowglobe/client/__init__.py +17 -0
- snowglobe/client/src/app.py +732 -0
- snowglobe/client/src/cli.py +736 -0
- snowglobe/client/src/cli_utils.py +361 -0
- snowglobe/client/src/config.py +213 -0
- snowglobe/client/src/models.py +37 -0
- snowglobe/client/src/project_manager.py +290 -0
- snowglobe/client/src/stats.py +53 -0
- snowglobe/client/src/utils.py +117 -0
- snowglobe-0.4.0.dist-info/METADATA +128 -0
- snowglobe-0.4.0.dist-info/RECORD +15 -0
- snowglobe-0.4.0.dist-info/WHEEL +5 -0
- snowglobe-0.4.0.dist-info/entry_points.txt +2 -0
- snowglobe-0.4.0.dist-info/licenses/LICENSE +21 -0
- snowglobe-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,290 @@
|
|
1
|
+
"""
|
2
|
+
Project and agent management for the new clean file structure
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import os
|
7
|
+
import re
|
8
|
+
from datetime import datetime
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
11
|
+
|
12
|
+
SNOWGLOBE_DIR = ".snowglobe"
|
13
|
+
AGENTS_FILE = "agents.json"
|
14
|
+
CONFIG_FILE = "config.json"
|
15
|
+
|
16
|
+
|
17
|
+
class ProjectManager:
|
18
|
+
"""Manages snowglobe project structure and agent mappings"""
|
19
|
+
|
20
|
+
def __init__(self, project_root: str = None):
|
21
|
+
self.project_root = Path(project_root or os.getcwd())
|
22
|
+
self.snowglobe_dir = self.project_root / SNOWGLOBE_DIR
|
23
|
+
self.agents_file = self.snowglobe_dir / AGENTS_FILE
|
24
|
+
self.config_file = self.snowglobe_dir / CONFIG_FILE
|
25
|
+
|
26
|
+
def ensure_project_structure(self) -> None:
|
27
|
+
"""Create .snowglobe directory if it doesn't exist"""
|
28
|
+
self.snowglobe_dir.mkdir(exist_ok=True)
|
29
|
+
|
30
|
+
# Create agents.json if it doesn't exist
|
31
|
+
if not self.agents_file.exists():
|
32
|
+
with open(self.agents_file, "w") as f:
|
33
|
+
json.dump({}, f, indent=2)
|
34
|
+
|
35
|
+
def load_agents_mapping(self) -> Dict[str, Dict[str, Any]]:
|
36
|
+
"""Load agent UUID mappings from .snowglobe/agents.json"""
|
37
|
+
if not self.agents_file.exists():
|
38
|
+
return {}
|
39
|
+
|
40
|
+
try:
|
41
|
+
with open(self.agents_file, "r") as f:
|
42
|
+
return json.load(f)
|
43
|
+
except (json.JSONDecodeError, IOError):
|
44
|
+
# Return empty dict if file is corrupted
|
45
|
+
return {}
|
46
|
+
|
47
|
+
def save_agents_mapping(self, mapping: Dict[str, Dict[str, Any]]) -> None:
|
48
|
+
"""Save agent UUID mappings to .snowglobe/agents.json"""
|
49
|
+
# Only ensure directory exists, not the full structure to avoid recursion
|
50
|
+
self.snowglobe_dir.mkdir(exist_ok=True)
|
51
|
+
|
52
|
+
with open(self.agents_file, "w") as f:
|
53
|
+
json.dump(mapping, f, indent=2, sort_keys=True)
|
54
|
+
|
55
|
+
def add_agent_mapping(self, filename: str, uuid: str, name: str) -> None:
|
56
|
+
"""Add a new agent mapping"""
|
57
|
+
mapping = self.load_agents_mapping()
|
58
|
+
|
59
|
+
mapping[filename] = {
|
60
|
+
"uuid": uuid,
|
61
|
+
"name": name,
|
62
|
+
"created": datetime.utcnow().isoformat() + "Z",
|
63
|
+
}
|
64
|
+
|
65
|
+
self.save_agents_mapping(mapping)
|
66
|
+
|
67
|
+
def get_agent_by_filename(self, filename: str) -> Optional[Dict[str, Any]]:
|
68
|
+
"""Get agent info by filename"""
|
69
|
+
mapping = self.load_agents_mapping()
|
70
|
+
return mapping.get(filename)
|
71
|
+
|
72
|
+
def get_agent_by_uuid(self, uuid: str) -> Optional[Tuple[str, Dict[str, Any]]]:
|
73
|
+
"""Get agent filename and info by UUID"""
|
74
|
+
mapping = self.load_agents_mapping()
|
75
|
+
|
76
|
+
for filename, info in mapping.items():
|
77
|
+
if info.get("uuid") == uuid:
|
78
|
+
return filename, info
|
79
|
+
|
80
|
+
return None
|
81
|
+
|
82
|
+
def list_agents(self) -> List[Tuple[str, Dict[str, Any]]]:
|
83
|
+
"""List all agents in the project"""
|
84
|
+
mapping = self.load_agents_mapping()
|
85
|
+
agents = []
|
86
|
+
|
87
|
+
for filename, info in mapping.items():
|
88
|
+
# Check if the file actually exists
|
89
|
+
file_path = self.project_root / filename
|
90
|
+
if file_path.exists():
|
91
|
+
agents.append((filename, info))
|
92
|
+
|
93
|
+
return agents
|
94
|
+
|
95
|
+
def remove_agent_mapping(self, filename: str) -> bool:
|
96
|
+
"""Remove an agent mapping"""
|
97
|
+
mapping = self.load_agents_mapping()
|
98
|
+
|
99
|
+
if filename in mapping:
|
100
|
+
del mapping[filename]
|
101
|
+
self.save_agents_mapping(mapping)
|
102
|
+
return True
|
103
|
+
|
104
|
+
return False
|
105
|
+
|
106
|
+
def sanitize_filename(self, name: str, default: str = "agent_wrapper") -> str:
|
107
|
+
"""Convert agent name to safe filename"""
|
108
|
+
if not name or not name.strip():
|
109
|
+
return f"{default}.py"
|
110
|
+
|
111
|
+
# Replace spaces and special chars with underscores
|
112
|
+
safe_name = re.sub(r"[^\w\s-]", "", name)
|
113
|
+
safe_name = re.sub(r"[-\s]+", "_", safe_name)
|
114
|
+
safe_name = safe_name.lower().strip("_")
|
115
|
+
|
116
|
+
# Ensure it's not empty and has .py extension
|
117
|
+
if not safe_name:
|
118
|
+
safe_name = default
|
119
|
+
|
120
|
+
return f"{safe_name}.py"
|
121
|
+
|
122
|
+
def find_available_filename(self, preferred_name: str) -> str:
|
123
|
+
"""Find an available filename, adding numbers if needed"""
|
124
|
+
base_name = preferred_name
|
125
|
+
if base_name.endswith(".py"):
|
126
|
+
base_name = base_name[:-3]
|
127
|
+
|
128
|
+
counter = 1
|
129
|
+
filename = f"{base_name}.py"
|
130
|
+
|
131
|
+
while (self.project_root / filename).exists():
|
132
|
+
filename = f"{base_name}_{counter}.py"
|
133
|
+
counter += 1
|
134
|
+
|
135
|
+
return filename
|
136
|
+
|
137
|
+
def load_config(self) -> Dict[str, Any]:
|
138
|
+
"""Load configuration from .snowglobe/config.json"""
|
139
|
+
if not self.config_file.exists():
|
140
|
+
return {}
|
141
|
+
|
142
|
+
try:
|
143
|
+
with open(self.config_file, "r") as f:
|
144
|
+
return json.load(f)
|
145
|
+
except (json.JSONDecodeError, IOError):
|
146
|
+
return {}
|
147
|
+
|
148
|
+
def save_config(self, config: Dict[str, Any]) -> None:
|
149
|
+
"""Save configuration to .snowglobe/config.json"""
|
150
|
+
self.snowglobe_dir.mkdir(exist_ok=True)
|
151
|
+
|
152
|
+
with open(self.config_file, "w") as f:
|
153
|
+
json.dump(config, f, indent=2, sort_keys=True)
|
154
|
+
|
155
|
+
def get_api_key(self) -> Optional[str]:
|
156
|
+
"""Get API key from config or environment"""
|
157
|
+
# Check environment first
|
158
|
+
api_key = os.getenv("SNOWGLOBE_API_KEY") or os.getenv("GUARDRAILS_API_KEY")
|
159
|
+
if api_key:
|
160
|
+
return api_key
|
161
|
+
|
162
|
+
# Check .snowglobe/config.json
|
163
|
+
config = self.load_config()
|
164
|
+
api_key = config.get("api_key")
|
165
|
+
if api_key:
|
166
|
+
return api_key
|
167
|
+
|
168
|
+
# Check legacy .snowgloberc in current directory
|
169
|
+
rc_path = self.project_root / ".snowgloberc"
|
170
|
+
if rc_path.exists():
|
171
|
+
try:
|
172
|
+
with open(rc_path, "r") as f:
|
173
|
+
for line in f:
|
174
|
+
if line.startswith("SNOWGLOBE_API_KEY="):
|
175
|
+
return line.strip().split("=", 1)[1]
|
176
|
+
except IOError:
|
177
|
+
pass
|
178
|
+
|
179
|
+
return None
|
180
|
+
|
181
|
+
def set_api_key(self, api_key: str) -> None:
|
182
|
+
"""Set API key in config"""
|
183
|
+
config = self.load_config()
|
184
|
+
config["api_key"] = api_key
|
185
|
+
self.save_config(config)
|
186
|
+
|
187
|
+
def get_control_plane_url(self) -> str:
|
188
|
+
"""Get control plane URL from config or environment"""
|
189
|
+
# Check environment first
|
190
|
+
url = os.getenv("CONTROL_PLANE_URL")
|
191
|
+
if url:
|
192
|
+
return url
|
193
|
+
|
194
|
+
# Check .snowglobe/config.json
|
195
|
+
config = self.load_config()
|
196
|
+
url = config.get("control_plane_url")
|
197
|
+
if url:
|
198
|
+
return url
|
199
|
+
|
200
|
+
# Check legacy .snowgloberc in current directory
|
201
|
+
rc_path = self.project_root / ".snowgloberc"
|
202
|
+
if rc_path.exists():
|
203
|
+
try:
|
204
|
+
with open(rc_path, "r") as f:
|
205
|
+
for line in f:
|
206
|
+
if line.startswith("CONTROL_PLANE_URL="):
|
207
|
+
return line.strip().split("=", 1)[1]
|
208
|
+
except IOError:
|
209
|
+
pass
|
210
|
+
|
211
|
+
return "https://api.snowglobe.guardrailsai.com"
|
212
|
+
|
213
|
+
def set_control_plane_url(self, url: str) -> None:
|
214
|
+
"""Set control plane URL in config"""
|
215
|
+
config = self.load_config()
|
216
|
+
config["control_plane_url"] = url
|
217
|
+
self.save_config(config)
|
218
|
+
|
219
|
+
def migrate_legacy_config(self) -> bool:
|
220
|
+
"""Migrate .snowgloberc to .snowglobe/config.json"""
|
221
|
+
rc_path = self.project_root / ".snowgloberc"
|
222
|
+
if not rc_path.exists():
|
223
|
+
return False
|
224
|
+
|
225
|
+
config = self.load_config()
|
226
|
+
migrated = False
|
227
|
+
|
228
|
+
try:
|
229
|
+
with open(rc_path, "r") as f:
|
230
|
+
for line in f:
|
231
|
+
line = line.strip()
|
232
|
+
if line.startswith("SNOWGLOBE_API_KEY="):
|
233
|
+
api_key = line.split("=", 1)[1]
|
234
|
+
if api_key and not config.get("api_key"):
|
235
|
+
config["api_key"] = api_key
|
236
|
+
migrated = True
|
237
|
+
elif line.startswith("CONTROL_PLANE_URL="):
|
238
|
+
url = line.split("=", 1)[1]
|
239
|
+
if url and not config.get("control_plane_url"):
|
240
|
+
config["control_plane_url"] = url
|
241
|
+
migrated = True
|
242
|
+
|
243
|
+
if migrated:
|
244
|
+
self.save_config(config)
|
245
|
+
# Optionally remove the old file
|
246
|
+
# rc_path.unlink() # Uncomment to delete legacy file
|
247
|
+
|
248
|
+
return migrated
|
249
|
+
except IOError:
|
250
|
+
return False
|
251
|
+
|
252
|
+
def validate_project(self) -> Tuple[bool, List[str]]:
|
253
|
+
"""Validate project structure and return issues"""
|
254
|
+
issues = []
|
255
|
+
|
256
|
+
# Check if .snowglobe directory exists
|
257
|
+
if not self.snowglobe_dir.exists():
|
258
|
+
issues.append(
|
259
|
+
"No .snowglobe directory found. Run 'snowglobe-connect init' to set up."
|
260
|
+
)
|
261
|
+
return False, issues
|
262
|
+
|
263
|
+
# Check if agents.json exists and is valid
|
264
|
+
if not self.agents_file.exists():
|
265
|
+
issues.append("No agents.json file found in .snowglobe/")
|
266
|
+
else:
|
267
|
+
try:
|
268
|
+
mapping = self.load_agents_mapping()
|
269
|
+
|
270
|
+
# Check for orphaned files
|
271
|
+
for filename in mapping.keys():
|
272
|
+
file_path = self.project_root / filename
|
273
|
+
if not file_path.exists():
|
274
|
+
issues.append(f"Agent file missing: {filename}")
|
275
|
+
|
276
|
+
# Check for unmapped agent files
|
277
|
+
for py_file in self.project_root.glob("*_wrapper.py"):
|
278
|
+
if py_file.name not in mapping:
|
279
|
+
issues.append(f"Unmapped agent file found: {py_file.name}")
|
280
|
+
|
281
|
+
except Exception as e:
|
282
|
+
issues.append(f"Invalid agents.json file: {e}")
|
283
|
+
|
284
|
+
is_valid = len(issues) == 0
|
285
|
+
return is_valid, issues
|
286
|
+
|
287
|
+
|
288
|
+
def get_project_manager(project_root: str = None) -> ProjectManager:
|
289
|
+
"""Get a ProjectManager instance"""
|
290
|
+
return ProjectManager(project_root)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
"""
|
2
|
+
Shared statistics tracking for Snowglobe client.
|
3
|
+
This module avoids circular imports by providing a neutral location
|
4
|
+
for stats that both app.py and cli.py need to access.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import datetime
|
8
|
+
|
9
|
+
# Tracking state
|
10
|
+
ui_stats = {
|
11
|
+
"last_activity_time": None,
|
12
|
+
"start_time": None,
|
13
|
+
"total_messages": 0,
|
14
|
+
"experiment_totals": {}, # experiment_name -> total_count
|
15
|
+
}
|
16
|
+
|
17
|
+
|
18
|
+
def initialize_stats():
|
19
|
+
"""Initialize stats tracking when server starts"""
|
20
|
+
ui_stats["start_time"] = datetime.datetime.now()
|
21
|
+
|
22
|
+
|
23
|
+
def track_batch_completion(experiment_name: str, count: int):
|
24
|
+
"""Track completed batch of scenarios"""
|
25
|
+
ui_stats["total_messages"] += count
|
26
|
+
if experiment_name not in ui_stats["experiment_totals"]:
|
27
|
+
ui_stats["experiment_totals"][experiment_name] = 0
|
28
|
+
ui_stats["experiment_totals"][experiment_name] += count
|
29
|
+
ui_stats["last_activity_time"] = datetime.datetime.now()
|
30
|
+
|
31
|
+
|
32
|
+
def get_shutdown_stats():
|
33
|
+
"""Get stats for graceful shutdown summary"""
|
34
|
+
if not ui_stats["start_time"]:
|
35
|
+
return None
|
36
|
+
|
37
|
+
uptime = datetime.datetime.now() - ui_stats["start_time"]
|
38
|
+
hours = int(uptime.total_seconds() // 3600)
|
39
|
+
minutes = int((uptime.total_seconds() % 3600) // 60)
|
40
|
+
seconds = int(uptime.total_seconds() % 60)
|
41
|
+
|
42
|
+
if hours > 0:
|
43
|
+
uptime_str = f"{hours}h {minutes}m {seconds}s"
|
44
|
+
elif minutes > 0:
|
45
|
+
uptime_str = f"{minutes}m {seconds}s"
|
46
|
+
else:
|
47
|
+
uptime_str = f"{seconds}s"
|
48
|
+
|
49
|
+
return {
|
50
|
+
"total_messages": ui_stats["total_messages"],
|
51
|
+
"experiment_totals": ui_stats["experiment_totals"],
|
52
|
+
"uptime": uptime_str,
|
53
|
+
}
|
@@ -0,0 +1,117 @@
|
|
1
|
+
from logging import getLogger
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
|
5
|
+
from .config import config
|
6
|
+
from .models import SnowglobeData, SnowglobeMessage
|
7
|
+
|
8
|
+
LOGGER = getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
async def fetch_experiments(app_id: str = None) -> list[dict]:
|
12
|
+
"""
|
13
|
+
Fetch experiments from the Snowglobe server.
|
14
|
+
|
15
|
+
Returns:
|
16
|
+
list[dict]: A list of experiments.
|
17
|
+
"""
|
18
|
+
async with httpx.AsyncClient() as client:
|
19
|
+
experiments_url = f"{config.CONTROL_PLANE_URL}/api/experiments?&evaluated=false"
|
20
|
+
if app_id:
|
21
|
+
experiments_url += f"&appId={config.APPLICATION_ID}"
|
22
|
+
experiments_response = await client.get(
|
23
|
+
experiments_url,
|
24
|
+
headers={"x-api-key": config.API_KEY},
|
25
|
+
)
|
26
|
+
|
27
|
+
if not experiments_response.status_code == 200:
|
28
|
+
try:
|
29
|
+
message = experiments_response.json().get("message")
|
30
|
+
except Exception:
|
31
|
+
message = experiments_response.text
|
32
|
+
LOGGER.error(f"Error fetching experiments: {message}")
|
33
|
+
raise Exception(
|
34
|
+
f"{experiments_response.status_code} - {message or 'Unknown error'}"
|
35
|
+
)
|
36
|
+
experiments = experiments_response.json()
|
37
|
+
return experiments
|
38
|
+
|
39
|
+
|
40
|
+
async def fetch_messages(*, test) -> list[SnowglobeMessage]:
|
41
|
+
"""
|
42
|
+
Fetch messages from the Snowglobe server for a given test.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
test (str): The test identifier.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
list[SnowglobeMessage]: A list of messages associated with the test.
|
49
|
+
"""
|
50
|
+
# init messages
|
51
|
+
messages = [
|
52
|
+
SnowglobeMessage(
|
53
|
+
role="user",
|
54
|
+
content=test["prompt"],
|
55
|
+
snowglobe_data=SnowglobeData(
|
56
|
+
conversation_id=test["conversation_id"],
|
57
|
+
test_id=test["id"],
|
58
|
+
),
|
59
|
+
),
|
60
|
+
]
|
61
|
+
# if full turn append response
|
62
|
+
if "response" in test and test["response"]:
|
63
|
+
messages.append(
|
64
|
+
SnowglobeMessage(
|
65
|
+
role="assistant",
|
66
|
+
content=test["response"],
|
67
|
+
snowglobe_data=SnowglobeData(
|
68
|
+
conversation_id=test["conversation_id"],
|
69
|
+
test_id=test["id"],
|
70
|
+
),
|
71
|
+
)
|
72
|
+
)
|
73
|
+
|
74
|
+
# build rest of messages
|
75
|
+
parent_id = test.get("parent_test_id")
|
76
|
+
async with httpx.AsyncClient() as client:
|
77
|
+
while parent_id:
|
78
|
+
# get parent test
|
79
|
+
parent_test_response = await client.get(
|
80
|
+
f"{config.CONTROL_PLANE_URL}/api/experiments/{test['experiment_id']}/tests/{parent_id}",
|
81
|
+
headers={"x-api-key": config.API_KEY},
|
82
|
+
)
|
83
|
+
|
84
|
+
if not parent_test_response.status_code == 200:
|
85
|
+
raise Exception(
|
86
|
+
f"Error fetching parent test {parent_id}: {parent_test_response.text}"
|
87
|
+
)
|
88
|
+
|
89
|
+
parent_test = parent_test_response.json()
|
90
|
+
|
91
|
+
parent_id = parent_test.get("parent_test_id")
|
92
|
+
|
93
|
+
messages.insert(
|
94
|
+
0,
|
95
|
+
SnowglobeMessage(
|
96
|
+
role="assistant",
|
97
|
+
content=parent_test["response"],
|
98
|
+
snowglobe_data=SnowglobeData(
|
99
|
+
conversation_id=parent_test["conversation_id"],
|
100
|
+
test_id=parent_test["id"],
|
101
|
+
),
|
102
|
+
),
|
103
|
+
)
|
104
|
+
|
105
|
+
messages.insert(
|
106
|
+
0,
|
107
|
+
SnowglobeMessage(
|
108
|
+
role="user",
|
109
|
+
content=parent_test["prompt"],
|
110
|
+
snowglobe_data=SnowglobeData(
|
111
|
+
conversation_id=parent_test["conversation_id"],
|
112
|
+
test_id=parent_test["id"],
|
113
|
+
),
|
114
|
+
),
|
115
|
+
)
|
116
|
+
|
117
|
+
return messages
|
@@ -0,0 +1,128 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: snowglobe
|
3
|
+
Version: 0.4.0
|
4
|
+
Summary: client server for usage with snowglobe experiments
|
5
|
+
Author-email: Guardrails AI <contact@guardrailsai.com>
|
6
|
+
License: MIT License
|
7
|
+
|
8
|
+
Copyright (c) 2024 Guardrails AI
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
|
28
|
+
Requires-Python: >=3.9
|
29
|
+
Description-Content-Type: text/markdown
|
30
|
+
License-File: LICENSE
|
31
|
+
Requires-Dist: typer>=0.9.0
|
32
|
+
Requires-Dist: fastapi>=0.115.6
|
33
|
+
Requires-Dist: uvicorn[standard]>=0.23.0
|
34
|
+
Requires-Dist: requests>=2.31.0
|
35
|
+
Requires-Dist: pydantic>=2.11.5
|
36
|
+
Requires-Dist: APScheduler==4.0.0a6
|
37
|
+
Requires-Dist: httpx==0.28.1
|
38
|
+
Requires-Dist: rich>=13.0.0
|
39
|
+
Provides-Extra: dev
|
40
|
+
Requires-Dist: ruff<0.2.0,>=0.1.0; extra == "dev"
|
41
|
+
Requires-Dist: mypy<2.0.0,>=1.5.0; extra == "dev"
|
42
|
+
Requires-Dist: pre-commit>=4.1.0; extra == "dev"
|
43
|
+
Requires-Dist: coverage>=7.6.12; extra == "dev"
|
44
|
+
Dynamic: license-file
|
45
|
+
|
46
|
+
# Snowlgobe Connect SDK
|
47
|
+
|
48
|
+
The Snowglobe Connect SDK helps you connect your AI agents to Snowglobe. It sends simulated user messages to your LLM-based application during experiments. Your application should process these messages and return a response, enabling simulated conversations and custom code based risk assessment.
|
49
|
+
|
50
|
+
## Installation
|
51
|
+
|
52
|
+
```bash
|
53
|
+
# Paste Guardrails Client or extract from guardrailsrc
|
54
|
+
export GUARDRAILS_TOKEN=$(cat ~/.guardrailsrc| awk -F 'token=' '{print $2}' | awk '{print $1}' | tr -d '\n')
|
55
|
+
```
|
56
|
+
|
57
|
+
```
|
58
|
+
# Install client
|
59
|
+
pip install -U --index-url="https://__token__:$GUARDRAILS_TOKEN@pypi.guardrailsai.com/simple" \
|
60
|
+
--extra-index-url="https://pypi.org/simple" snowglobe-connect
|
61
|
+
```
|
62
|
+
|
63
|
+
If using uv, set the `--prerelease=allow` flag
|
64
|
+
```
|
65
|
+
pip install -U --index-url="https://__token__:$GUARDRAILS_TOKEN@pypi.guardrailsai.com/simple" \
|
66
|
+
--extra-index-url="https://pypi.org/simple" --prerelease=allow snowglobe-connect
|
67
|
+
```
|
68
|
+
|
69
|
+
|
70
|
+
## `snowglobe-connect` commands
|
71
|
+
|
72
|
+
```bash
|
73
|
+
snowglobe-connect auth # Sets up your API key
|
74
|
+
snowglobe-connect init # Initializes a new agent connection and creates an agent wrapper file
|
75
|
+
snowglobe-connect test # Tests your agent connection
|
76
|
+
snowglobe-connect start # Starts the process of processing simulated user messages
|
77
|
+
snowglobe-connect --help
|
78
|
+
```
|
79
|
+
|
80
|
+
When using one of our specific preview environments in .snowgloberc one can override our server's URL with:
|
81
|
+
|
82
|
+
```bash
|
83
|
+
CONTROL_PLANE_URL=
|
84
|
+
```
|
85
|
+
|
86
|
+
## Sample custom llm usage in agent wrapper file
|
87
|
+
|
88
|
+
Each agent wrapper file resides in the root directory of your project, and is named after the agent (e.g. `My Agent Name` becomes `my_agent_name.py`).
|
89
|
+
|
90
|
+
```python
|
91
|
+
from snowglobe.client import CompletionRequest, CompletionFunctionOutputs
|
92
|
+
from openai import OpenAI
|
93
|
+
import os
|
94
|
+
|
95
|
+
client = OpenAI(api_key=os.getenv("SNOWGLOBE_API_KEY"))
|
96
|
+
|
97
|
+
def process_scenario(request: CompletionRequest) -> CompletionFunctionOutputs:
|
98
|
+
"""
|
99
|
+
Process a scenario request from Snowglobe.
|
100
|
+
|
101
|
+
This function is called by the Snowglobe client to process requests. It should return a
|
102
|
+
CompletionFunctionOutputs object with the response content.
|
103
|
+
|
104
|
+
Example CompletionRequest:
|
105
|
+
CompletionRequest(
|
106
|
+
messages=[
|
107
|
+
SnowglobeMessage(role="user", content="Hello, how are you?", snowglobe_data=None),
|
108
|
+
]
|
109
|
+
)
|
110
|
+
|
111
|
+
Example CompletionFunctionOutputs:
|
112
|
+
CompletionFunctionOutputs(response="This is a string response from your application")
|
113
|
+
|
114
|
+
Args:
|
115
|
+
request (CompletionRequest): The request object containing the messages.
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
CompletionFunctionOutputs: The response object with the generated content.
|
119
|
+
"""
|
120
|
+
|
121
|
+
# Process the request using the messages. Example:
|
122
|
+
messages = request.to_openai_messages()
|
123
|
+
response = client.chat.completions.create(
|
124
|
+
model="gpt-4o-mini",
|
125
|
+
messages=messages
|
126
|
+
)
|
127
|
+
return CompletionFunctionOutputs(response=response.choices[0].message.content)
|
128
|
+
```
|
@@ -0,0 +1,15 @@
|
|
1
|
+
snowglobe/client/__init__.py,sha256=kzp9wPUUYBXqDSKZbfmD4vrAQvrWSW5HOvtpFlEJWfs,353
|
2
|
+
snowglobe/client/src/app.py,sha256=PZCTwF3n_2Bi825dOflGeN8GSjuf5ebKLSxV7S90TxA,30289
|
3
|
+
snowglobe/client/src/cli.py,sha256=f8AKB0mAcMK-63aZczs4LxnB33RqoxKPEnv2w1lgu1c,25159
|
4
|
+
snowglobe/client/src/cli_utils.py,sha256=PRWpbrALfTc3fpAXl32pfyZWkLYi_3N03csTQR1wnjc,11911
|
5
|
+
snowglobe/client/src/config.py,sha256=HAJD7RkO6IB_mFkmGLpy9Ma3sB0KpZE7zmGsa9K__iE,9284
|
6
|
+
snowglobe/client/src/models.py,sha256=BX310WrDN9Fd8v68me3XGL_ic1ulvjCrZyIT2ND1eUo,866
|
7
|
+
snowglobe/client/src/project_manager.py,sha256=Ze-qs4dQI2kIV-PmtWZ1b67hMUfsnsMHus90aT8HOow,9970
|
8
|
+
snowglobe/client/src/stats.py,sha256=IdaXroOZBmvLVa_p9pDE6hsxsc7-fBEDnLf8O6Ch0GA,1596
|
9
|
+
snowglobe/client/src/utils.py,sha256=U0nQjTjO28OghG7lV6BuI_2MkcsOhu31Nz00nCJU4sM,3670
|
10
|
+
snowglobe-0.4.0.dist-info/licenses/LICENSE,sha256=S90V6iFU5ZeSg44JQYS1To3pa7ZEobrHc_t483qSKSI,1070
|
11
|
+
snowglobe-0.4.0.dist-info/METADATA,sha256=OEuNnqFkNHhu3z0cgORX17kyKGOhoAvo7IxUfkopMTM,4921
|
12
|
+
snowglobe-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
13
|
+
snowglobe-0.4.0.dist-info/entry_points.txt,sha256=mqx4mTwFPHttjctE2ceYTYWCCIG30Ji2C89aaCYgHcM,71
|
14
|
+
snowglobe-0.4.0.dist-info/top_level.txt,sha256=PoyYihnCBjRyjeIT19yBcE47JTe7i1OwRXvJ4d5EohM,10
|
15
|
+
snowglobe-0.4.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Guardrails AI
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
snowglobe
|