snowglobe 0.4.7__tar.gz → 0.4.9__tar.gz
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-0.4.7/src/snowglobe.egg-info → snowglobe-0.4.9}/PKG-INFO +15 -1
- {snowglobe-0.4.7 → snowglobe-0.4.9}/README.md +14 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/pyproject.toml +1 -1
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/src/app.py +119 -22
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/src/cli.py +29 -11
- snowglobe-0.4.9/src/snowglobe/client/src/telemetry.py +146 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9/src/snowglobe.egg-info}/PKG-INFO +15 -1
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe.egg-info/SOURCES.txt +1 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/LICENSE +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/setup.cfg +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/__init__.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/src/cli_utils.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/src/config.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/src/models.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/src/project_manager.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/src/stats.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe/client/src/utils.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe.egg-info/dependency_links.txt +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe.egg-info/entry_points.txt +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe.egg-info/requires.txt +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/src/snowglobe.egg-info/top_level.txt +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/tests/test_app.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/tests/test_cli.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/tests/test_config.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/tests/test_heartbeat.py +0 -0
- {snowglobe-0.4.7 → snowglobe-0.4.9}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: snowglobe
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.9
|
4
4
|
Summary: client server for usage with snowglobe experiments
|
5
5
|
Author-email: Guardrails AI <contact@guardrailsai.com>
|
6
6
|
License: MIT License
|
@@ -119,3 +119,17 @@ def process_scenario(request: CompletionRequest) -> CompletionFunctionOutputs:
|
|
119
119
|
)
|
120
120
|
return CompletionFunctionOutputs(response=response.choices[0].message.content)
|
121
121
|
```
|
122
|
+
|
123
|
+
## Tracing with MLflow
|
124
|
+
The Snowglobe Connect SDK has MLflow tracing built in! Simply `pip install mlflow` and the sdk will take care of the rest. Read more about MLflow's tracing capability for GenAI Apps [here](https://mlflow.org/docs/latest/genai/tracing/app-instrumentation/).
|
125
|
+
|
126
|
+
### Enhancing Snowglobe Connect SDK's Traces with Autologging
|
127
|
+
You can turn on mlflow autologging in your app to add additional context to the traces the Snowglobe Connect SDK captures. In you app's entry point simply call the appropriate autolog method for the LLM provider you're using. The below example shows how to enable this for LiteLLM:
|
128
|
+
```py
|
129
|
+
import mlflow
|
130
|
+
|
131
|
+
mlflow.litellm.autolog()
|
132
|
+
```
|
133
|
+
|
134
|
+
### Disable Snowglobe Connect SDK's MLflow Tracing
|
135
|
+
If you already use MLflow and don't want the Snowglobe Connect SDK to capture additional traces, you can disable this feature by setting the `SNOWGLOBE_DISABLE_MLFLOW_TRACING` environment variable to `true`.
|
@@ -74,3 +74,17 @@ def process_scenario(request: CompletionRequest) -> CompletionFunctionOutputs:
|
|
74
74
|
)
|
75
75
|
return CompletionFunctionOutputs(response=response.choices[0].message.content)
|
76
76
|
```
|
77
|
+
|
78
|
+
## Tracing with MLflow
|
79
|
+
The Snowglobe Connect SDK has MLflow tracing built in! Simply `pip install mlflow` and the sdk will take care of the rest. Read more about MLflow's tracing capability for GenAI Apps [here](https://mlflow.org/docs/latest/genai/tracing/app-instrumentation/).
|
80
|
+
|
81
|
+
### Enhancing Snowglobe Connect SDK's Traces with Autologging
|
82
|
+
You can turn on mlflow autologging in your app to add additional context to the traces the Snowglobe Connect SDK captures. In you app's entry point simply call the appropriate autolog method for the LLM provider you're using. The below example shows how to enable this for LiteLLM:
|
83
|
+
```py
|
84
|
+
import mlflow
|
85
|
+
|
86
|
+
mlflow.litellm.autolog()
|
87
|
+
```
|
88
|
+
|
89
|
+
### Disable Snowglobe Connect SDK's MLflow Tracing
|
90
|
+
If you already use MLflow and don't want the Snowglobe Connect SDK to capture additional traces, you can disable this feature by setting the `SNOWGLOBE_DISABLE_MLFLOW_TRACING` environment variable to `true`.
|
@@ -5,6 +5,7 @@ import importlib.util
|
|
5
5
|
import json
|
6
6
|
import logging
|
7
7
|
import os
|
8
|
+
import sys
|
8
9
|
import time
|
9
10
|
import traceback
|
10
11
|
from collections import defaultdict, deque
|
@@ -13,6 +14,7 @@ from functools import wraps
|
|
13
14
|
from logging import getLogger
|
14
15
|
from typing import Dict
|
15
16
|
from urllib.parse import quote_plus
|
17
|
+
import uuid
|
16
18
|
|
17
19
|
import httpx
|
18
20
|
import uvicorn
|
@@ -20,6 +22,8 @@ from apscheduler import AsyncScheduler
|
|
20
22
|
from apscheduler.triggers.interval import IntervalTrigger
|
21
23
|
from fastapi import FastAPI, HTTPException, Request
|
22
24
|
|
25
|
+
from snowglobe.client.src.telemetry import trace_completion_fn, trace_risk_evaluation_fn
|
26
|
+
|
23
27
|
from .cli_utils import info, shutdown_manager
|
24
28
|
from .config import config, get_api_key_or_raise
|
25
29
|
from .models import CompletionFunctionOutputs, CompletionRequest, RiskEvaluationRequest
|
@@ -126,16 +130,32 @@ async def process_application_heartbeat(app_id):
|
|
126
130
|
try:
|
127
131
|
prompt = "Hello from Snowglobe!"
|
128
132
|
test_request = CompletionRequest(messages=[{"role": "user", "content": prompt}])
|
129
|
-
|
133
|
+
heartbeat_id = uuid.uuid4().hex
|
134
|
+
agent = apps.get(app_id, {})
|
135
|
+
agent_name = agent.get("name", "")
|
136
|
+
completion_fn = agent.get("completion_fn")
|
130
137
|
if not completion_fn:
|
131
138
|
LOGGER.warning(
|
132
139
|
f"No completion function found for application {app_id}. Skipping heartbeat."
|
133
140
|
)
|
134
141
|
return
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
142
|
+
|
143
|
+
@trace_completion_fn(
|
144
|
+
agent_name=agent_name,
|
145
|
+
conversation_id=heartbeat_id,
|
146
|
+
message_id=heartbeat_id,
|
147
|
+
session_id=heartbeat_id,
|
148
|
+
simulation_name=f"{agent_name} Heartbeat",
|
149
|
+
span_type="snowglobe/heartbeat",
|
150
|
+
)
|
151
|
+
async def run_completion_fn(completion_request: CompletionRequest):
|
152
|
+
if asyncio.iscoroutinefunction(completion_fn):
|
153
|
+
response = await completion_fn(completion_request)
|
154
|
+
else:
|
155
|
+
response = completion_fn(completion_request)
|
156
|
+
return response
|
157
|
+
|
158
|
+
response = await run_completion_fn(test_request)
|
139
159
|
if not isinstance(response, CompletionFunctionOutputs):
|
140
160
|
LOGGER.error(
|
141
161
|
f"Completion function for application {app_id} did not return a valid response. Expected CompletionFunctionOutputs, got {type(response)}"
|
@@ -200,19 +220,31 @@ async def process_application_heartbeat(app_id):
|
|
200
220
|
return connection_test_response.json()
|
201
221
|
|
202
222
|
|
203
|
-
async def process_risk_evaluation(test, risk_name):
|
223
|
+
async def process_risk_evaluation(test, risk_name, simulation_name, agent_name):
|
204
224
|
"""finds correct risk and calls the risk evaluation function and creates a risk evaluation for the test"""
|
205
225
|
start = time.time()
|
206
226
|
|
207
227
|
messages = await fetch_messages(test=test)
|
208
228
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
229
|
+
risk_eval_req = RiskEvaluationRequest(messages=messages)
|
230
|
+
|
231
|
+
@trace_risk_evaluation_fn(
|
232
|
+
agent_name=agent_name,
|
233
|
+
conversation_id=test["conversation_id"],
|
234
|
+
message_id=test["id"],
|
235
|
+
session_id=test["conversation_id"],
|
236
|
+
simulation_name=simulation_name,
|
237
|
+
span_type=f"snowglobe/risk-evaluation/{risk_name}",
|
238
|
+
risk_name=risk_name,
|
239
|
+
)
|
240
|
+
async def run_risk_evaluation_fn(risk_evaluation_request: RiskEvaluationRequest):
|
241
|
+
if asyncio.iscoroutinefunction(risks[risk_name]):
|
242
|
+
risk_evaluation = await risks[risk_name](risk_evaluation_request)
|
243
|
+
else:
|
244
|
+
risk_evaluation = risks[risk_name](risk_evaluation_request)
|
245
|
+
return risk_evaluation
|
215
246
|
|
247
|
+
risk_evaluation = await run_risk_evaluation_fn(risk_eval_req)
|
216
248
|
LOGGER.debug(f"Risk evaluation output: {risk_evaluation}")
|
217
249
|
|
218
250
|
# Extract fields from risk_evaluation object
|
@@ -248,16 +280,33 @@ async def process_risk_evaluation(test, risk_name):
|
|
248
280
|
raise Exception("Error posting risk evaluation, task is not healthy")
|
249
281
|
|
250
282
|
|
251
|
-
async def process_test(test, completion_fn, app_id):
|
283
|
+
async def process_test(test, completion_fn, app_id, simulation_name):
|
252
284
|
"""Processes a test by converting it to OpenAI style messages and calling the completion function"""
|
253
285
|
start = time.time()
|
254
286
|
# convert test to openai style messages
|
255
287
|
messages = await fetch_messages(test=test)
|
256
288
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
289
|
+
agent = apps.get(app_id, {})
|
290
|
+
agent_name = agent.get("name", "")
|
291
|
+
|
292
|
+
completion_req = CompletionRequest(messages=messages)
|
293
|
+
|
294
|
+
@trace_completion_fn(
|
295
|
+
agent_name=agent_name,
|
296
|
+
conversation_id=test["conversation_id"],
|
297
|
+
message_id=test["id"],
|
298
|
+
session_id=test["conversation_id"],
|
299
|
+
simulation_name=simulation_name,
|
300
|
+
span_type="snowglobe/completion",
|
301
|
+
)
|
302
|
+
async def run_completion_fn(completion_request: CompletionRequest):
|
303
|
+
if asyncio.iscoroutinefunction(completion_fn):
|
304
|
+
completionOutput = await completion_fn(completion_request)
|
305
|
+
else:
|
306
|
+
completionOutput = completion_fn(completion_request)
|
307
|
+
return completionOutput
|
308
|
+
|
309
|
+
completionOutput = await run_completion_fn(completion_req)
|
261
310
|
|
262
311
|
LOGGER.debug(f"Completion output: {completionOutput}")
|
263
312
|
|
@@ -387,7 +436,11 @@ async def poll_for_completions():
|
|
387
436
|
try:
|
388
437
|
completion_request = await httpx.AsyncClient().post(
|
389
438
|
f"{config.SNOWGLOBE_CLIENT_URL}/completion",
|
390
|
-
json={
|
439
|
+
json={
|
440
|
+
"test": test,
|
441
|
+
"app_id": app_id,
|
442
|
+
"simulation_name": experiment["name"],
|
443
|
+
},
|
391
444
|
timeout=30,
|
392
445
|
)
|
393
446
|
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
@@ -542,6 +595,31 @@ async def poll_for_risk_evaluations():
|
|
542
595
|
)
|
543
596
|
continue
|
544
597
|
experiment = experiment_request.json()
|
598
|
+
|
599
|
+
try:
|
600
|
+
app_request = await client.get(
|
601
|
+
f"{config.CONTROL_PLANE_URL}/api/applications/{experiment['app_id']}",
|
602
|
+
headers={"x-api-key": get_api_key_or_raise()},
|
603
|
+
)
|
604
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
605
|
+
if shutdown_manager.is_shutdown_requested():
|
606
|
+
LOGGER.debug(f"HTTP error during shutdown (expected): {e}")
|
607
|
+
return
|
608
|
+
else:
|
609
|
+
LOGGER.error(
|
610
|
+
f"Connection error fetching application {experiment['app_id']}: {e}"
|
611
|
+
)
|
612
|
+
continue
|
613
|
+
|
614
|
+
app_name = experiment["app_id"]
|
615
|
+
if not app_request.is_success:
|
616
|
+
LOGGER.error(
|
617
|
+
f"Error fetching application {experiment['app_id']}: {app_request.text}"
|
618
|
+
)
|
619
|
+
else:
|
620
|
+
application = app_request.json()
|
621
|
+
app_name = application["name"]
|
622
|
+
|
545
623
|
risk_eval_count = 0
|
546
624
|
|
547
625
|
for risk_name in risks.keys():
|
@@ -607,7 +685,12 @@ async def poll_for_risk_evaluations():
|
|
607
685
|
try:
|
608
686
|
risk_eval_response = await httpx.AsyncClient().post(
|
609
687
|
f"{config.SNOWGLOBE_CLIENT_URL}/risk-evaluation",
|
610
|
-
json={
|
688
|
+
json={
|
689
|
+
"test": test,
|
690
|
+
"risk_name": risk_name,
|
691
|
+
"simulation_name": experiment["name"],
|
692
|
+
"agent_name": app_name,
|
693
|
+
},
|
611
694
|
timeout=30,
|
612
695
|
)
|
613
696
|
except (
|
@@ -685,7 +768,17 @@ async def lifespan(app: FastAPI):
|
|
685
768
|
"agent_wrapper", agent_file_path
|
686
769
|
)
|
687
770
|
agent_module = importlib.util.module_from_spec(spec)
|
688
|
-
|
771
|
+
|
772
|
+
# Add current directory to path
|
773
|
+
sys_path_backup = sys.path.copy()
|
774
|
+
current_dir = os.getcwd()
|
775
|
+
if current_dir not in sys.path:
|
776
|
+
sys.path.insert(0, current_dir)
|
777
|
+
|
778
|
+
try:
|
779
|
+
spec.loader.exec_module(agent_module)
|
780
|
+
finally:
|
781
|
+
sys.path = sys_path_backup
|
689
782
|
|
690
783
|
if not hasattr(agent_module, "process_scenario"):
|
691
784
|
LOGGER.warning(
|
@@ -699,6 +792,7 @@ async def lifespan(app: FastAPI):
|
|
699
792
|
}
|
700
793
|
|
701
794
|
except Exception as e:
|
795
|
+
traceback.print_exc()
|
702
796
|
LOGGER.error(f"Error loading agent {filename}: {e}")
|
703
797
|
continue
|
704
798
|
|
@@ -836,6 +930,7 @@ def create_client():
|
|
836
930
|
completion_body = await request.json()
|
837
931
|
test = completion_body.get("test")
|
838
932
|
app_id = completion_body.get("app_id")
|
933
|
+
simulation_name = completion_body.get("simulation_name")
|
839
934
|
# both are required non empty strings
|
840
935
|
if not test or not app_id:
|
841
936
|
raise HTTPException(
|
@@ -850,7 +945,7 @@ def create_client():
|
|
850
945
|
completion_fn = apps.get(app_id, {}).get("completion_fn")
|
851
946
|
LOGGER.debug(f"Received test: {test['id']}")
|
852
947
|
|
853
|
-
await process_test(test, completion_fn, app_id)
|
948
|
+
await process_test(test, completion_fn, app_id, simulation_name)
|
854
949
|
return {"status": "processed"}
|
855
950
|
|
856
951
|
@app.post("/heartbeat")
|
@@ -890,10 +985,12 @@ def create_client():
|
|
890
985
|
body = await request.json()
|
891
986
|
test = body.get("test")
|
892
987
|
risk_name = body.get("risk_name")
|
988
|
+
simulation_name = body.get("simulation_name")
|
989
|
+
agent_name = body.get("agent_name")
|
893
990
|
LOGGER.debug(f"Received risk evaluation for test: {test['id']}")
|
894
991
|
|
895
992
|
# For now, just simulate processing
|
896
|
-
await process_risk_evaluation(test, risk_name)
|
993
|
+
await process_risk_evaluation(test, risk_name, simulation_name, agent_name)
|
897
994
|
return {"status": "risk evaluation processed"}
|
898
995
|
|
899
996
|
return app
|
@@ -6,6 +6,7 @@ import signal
|
|
6
6
|
import sys
|
7
7
|
import threading
|
8
8
|
import time
|
9
|
+
import uuid
|
9
10
|
import webbrowser
|
10
11
|
from importlib.metadata import version
|
11
12
|
from typing import Optional, Tuple
|
@@ -15,6 +16,8 @@ import uvicorn
|
|
15
16
|
from fastapi import FastAPI, Request
|
16
17
|
from fastapi.middleware.cors import CORSMiddleware
|
17
18
|
|
19
|
+
from snowglobe.client.src.telemetry import trace_completion_fn
|
20
|
+
|
18
21
|
# Import start_client lazily inside the start command to avoid config initialization
|
19
22
|
from .cli_utils import (
|
20
23
|
check_auth_status,
|
@@ -198,7 +201,7 @@ def test(
|
|
198
201
|
else:
|
199
202
|
info("Check your implementation and try again.")
|
200
203
|
docs_link(
|
201
|
-
"Troubleshooting guide", "https://
|
204
|
+
"Troubleshooting guide", "https://snowglobe.so/docs/troubleshooting"
|
202
205
|
)
|
203
206
|
raise typer.Exit(1)
|
204
207
|
|
@@ -231,7 +234,7 @@ def init(
|
|
231
234
|
if not is_auth:
|
232
235
|
error("Authentication required to initialize agents")
|
233
236
|
info("Please run 'snowglobe-connect auth' first to set up authentication")
|
234
|
-
docs_link("Setup guide", "https://
|
237
|
+
docs_link("Setup guide", "https://snowglobe.so/docs/setup")
|
235
238
|
raise typer.Exit(1)
|
236
239
|
|
237
240
|
success("Authenticated successfully")
|
@@ -256,9 +259,7 @@ def init(
|
|
256
259
|
raise typer.Exit(0)
|
257
260
|
elif selected == "new":
|
258
261
|
info("Creating new application not yet implemented in init command")
|
259
|
-
info(
|
260
|
-
"Please visit https://snowglobe.guardrails-ai.com/applications/create to create a new app"
|
261
|
-
)
|
262
|
+
info("Please visit https://snowglobe.guardrailsai.com/app to create a new app")
|
262
263
|
info("Then run this command again to select it")
|
263
264
|
raise typer.Exit(0)
|
264
265
|
|
@@ -388,10 +389,24 @@ def test_agent_wrapper(filename: str, app_id: str, app_name: str) -> Tuple[bool,
|
|
388
389
|
]
|
389
390
|
)
|
390
391
|
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
392
|
+
test_id = uuid.uuid4()
|
393
|
+
|
394
|
+
@trace_completion_fn(
|
395
|
+
agent_name=app_name,
|
396
|
+
conversation_id=test_id,
|
397
|
+
message_id=test_id,
|
398
|
+
session_id=test_id,
|
399
|
+
simulation_name=f"{app_name} CLI Test",
|
400
|
+
span_type="snowglobe/cli-test",
|
401
|
+
)
|
402
|
+
async def run_process_scenario(completion_request: CompletionRequest):
|
403
|
+
if asyncio.iscoroutinefunction(process_scenario):
|
404
|
+
response = asyncio.run(process_scenario(completion_request))
|
405
|
+
else:
|
406
|
+
response = process_scenario(completion_request)
|
407
|
+
return response
|
408
|
+
|
409
|
+
response = asyncio.run(run_process_scenario(test_request))
|
395
410
|
|
396
411
|
if hasattr(response, "response") and isinstance(response.response, str):
|
397
412
|
if response.response == "Your response here":
|
@@ -402,6 +417,7 @@ def test_agent_wrapper(filename: str, app_id: str, app_name: str) -> Tuple[bool,
|
|
402
417
|
|
403
418
|
except Exception as e:
|
404
419
|
import traceback
|
420
|
+
|
405
421
|
traceback.print_exc()
|
406
422
|
return False, f"Error: {str(e)}"
|
407
423
|
|
@@ -434,7 +450,7 @@ def enhanced_error_handler(status_code: int, operation: str = "operation") -> No
|
|
434
450
|
error("Authentication failed")
|
435
451
|
info("Your API key may be invalid or expired")
|
436
452
|
info("Run 'snowglobe-connect auth' to set up authentication")
|
437
|
-
docs_link("Authentication help", "https://
|
453
|
+
docs_link("Authentication help", "https://snowglobe.so/docs/auth")
|
438
454
|
elif status_code == 403:
|
439
455
|
error("Access forbidden")
|
440
456
|
info("You don't have permission for this operation")
|
@@ -606,6 +622,7 @@ def _create_auth_server(config_key: str, rc_path: str) -> FastAPI:
|
|
606
622
|
return {"written": True}
|
607
623
|
except Exception as e:
|
608
624
|
import traceback
|
625
|
+
|
609
626
|
traceback.print_exc()
|
610
627
|
error(f"Failed to process key configuration: {e}")
|
611
628
|
return {"error": "Failed to process key configuration request"}
|
@@ -624,7 +641,7 @@ def _show_auth_success_next_steps() -> None:
|
|
624
641
|
console.print("3. Start the client:")
|
625
642
|
console.print(" [bold green]snowglobe-connect start[/bold green]")
|
626
643
|
console.print()
|
627
|
-
docs_link("Getting started guide", "https://
|
644
|
+
docs_link("Getting started guide", "https://snowglobe.so/docs/getting-started")
|
628
645
|
|
629
646
|
|
630
647
|
def _poll_for_api_key(rc_path: str, timeout: int = 300) -> bool:
|
@@ -792,6 +809,7 @@ def start(
|
|
792
809
|
console.print()
|
793
810
|
except Exception:
|
794
811
|
import traceback
|
812
|
+
|
795
813
|
traceback.print_exc()
|
796
814
|
# Do not block startup if we cannot load agents mapping
|
797
815
|
pass
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import os
|
2
|
+
from importlib.metadata import version as importlib_version
|
3
|
+
from typing import Callable
|
4
|
+
from functools import wraps
|
5
|
+
|
6
|
+
from snowglobe.client.src.models import CompletionRequest, RiskEvaluationRequest
|
7
|
+
|
8
|
+
try:
|
9
|
+
import mlflow
|
10
|
+
import mlflow.tracing
|
11
|
+
|
12
|
+
mlflow.tracing.enable()
|
13
|
+
except ImportError:
|
14
|
+
mlflow = None
|
15
|
+
|
16
|
+
SNOWGLOBE_VERSION = importlib_version("snowglobe")
|
17
|
+
|
18
|
+
|
19
|
+
def trace_completion_fn(
|
20
|
+
*,
|
21
|
+
session_id: str,
|
22
|
+
conversation_id: str,
|
23
|
+
message_id: str,
|
24
|
+
simulation_name: str,
|
25
|
+
agent_name: str,
|
26
|
+
span_type: str,
|
27
|
+
):
|
28
|
+
def trace_decorator(completion_fn: Callable):
|
29
|
+
disable_mlflow = os.getenv("SNOWGLOBE_DISABLE_MLFLOW_TRACING") or ""
|
30
|
+
if mlflow and disable_mlflow.lower() != "true":
|
31
|
+
mlflow_experiment_name = (
|
32
|
+
os.getenv("MLFLOW_EXPERIMENT_NAME") or simulation_name
|
33
|
+
)
|
34
|
+
mlflow.set_experiment(mlflow_experiment_name)
|
35
|
+
|
36
|
+
mlflow_active_model_id = os.getenv("MLFLOW_ACTIVE_MODEL_ID")
|
37
|
+
if mlflow_active_model_id:
|
38
|
+
mlflow.set_active_model(model_id=mlflow_active_model_id)
|
39
|
+
else:
|
40
|
+
mlflow.set_active_model(name=agent_name)
|
41
|
+
|
42
|
+
span_attributes = {
|
43
|
+
"snowglobe.version": SNOWGLOBE_VERSION,
|
44
|
+
"type": span_type,
|
45
|
+
"session_id": session_id,
|
46
|
+
"conversation_id": conversation_id,
|
47
|
+
"message_id": message_id,
|
48
|
+
"simulation_name": simulation_name,
|
49
|
+
"agent_name": agent_name,
|
50
|
+
}
|
51
|
+
|
52
|
+
@mlflow.trace(
|
53
|
+
name=span_type,
|
54
|
+
span_type=span_type,
|
55
|
+
attributes=span_attributes,
|
56
|
+
)
|
57
|
+
@wraps(completion_fn)
|
58
|
+
async def completion_fn_wrapper(test_request: CompletionRequest):
|
59
|
+
try:
|
60
|
+
mlflow.update_current_trace(
|
61
|
+
metadata={"mlflow.trace.session": session_id},
|
62
|
+
tags={
|
63
|
+
"session_id": session_id,
|
64
|
+
"conversation_id": conversation_id,
|
65
|
+
"message_id": message_id,
|
66
|
+
"simulation_name": simulation_name,
|
67
|
+
"agent_name": agent_name,
|
68
|
+
},
|
69
|
+
)
|
70
|
+
response = await completion_fn(test_request)
|
71
|
+
return response
|
72
|
+
except Exception as e:
|
73
|
+
raise e
|
74
|
+
|
75
|
+
return completion_fn_wrapper
|
76
|
+
else:
|
77
|
+
return completion_fn
|
78
|
+
|
79
|
+
return trace_decorator
|
80
|
+
|
81
|
+
|
82
|
+
def trace_risk_evaluation_fn(
|
83
|
+
*,
|
84
|
+
session_id: str,
|
85
|
+
conversation_id: str,
|
86
|
+
message_id: str,
|
87
|
+
simulation_name: str,
|
88
|
+
agent_name: str,
|
89
|
+
span_type: str,
|
90
|
+
risk_name,
|
91
|
+
):
|
92
|
+
def trace_decorator(risk_evaluation_fn: Callable):
|
93
|
+
disable_mlflow = os.getenv("SNOWGLOBE_DISABLE_MLFLOW_TRACING") or ""
|
94
|
+
if mlflow and disable_mlflow.lower() != "true":
|
95
|
+
mlflow_experiment_name = (
|
96
|
+
os.getenv("MLFLOW_EXPERIMENT_NAME") or simulation_name
|
97
|
+
)
|
98
|
+
mlflow.set_experiment(mlflow_experiment_name)
|
99
|
+
|
100
|
+
mlflow_active_model_id = os.getenv("MLFLOW_ACTIVE_MODEL_ID")
|
101
|
+
if mlflow_active_model_id:
|
102
|
+
mlflow.set_active_model(model_id=mlflow_active_model_id)
|
103
|
+
else:
|
104
|
+
mlflow.set_active_model(name=agent_name)
|
105
|
+
span_attributes = {
|
106
|
+
"snowglobe.version": SNOWGLOBE_VERSION,
|
107
|
+
"type": span_type,
|
108
|
+
"session_id": session_id,
|
109
|
+
"conversation_id": conversation_id,
|
110
|
+
"message_id": message_id,
|
111
|
+
"simulation_name": simulation_name,
|
112
|
+
"agent_name": agent_name,
|
113
|
+
"risk_name": risk_name,
|
114
|
+
}
|
115
|
+
|
116
|
+
@mlflow.trace(
|
117
|
+
name=span_type,
|
118
|
+
span_type=span_type,
|
119
|
+
attributes=span_attributes,
|
120
|
+
)
|
121
|
+
@wraps(risk_evaluation_fn)
|
122
|
+
async def risk_evaluation_fn_wrapper(
|
123
|
+
risk_evaluation_request: RiskEvaluationRequest,
|
124
|
+
):
|
125
|
+
try:
|
126
|
+
mlflow.update_current_trace(
|
127
|
+
metadata={"mlflow.trace.session": session_id},
|
128
|
+
tags={
|
129
|
+
"session_id": session_id,
|
130
|
+
"conversation_id": conversation_id,
|
131
|
+
"message_id": message_id,
|
132
|
+
"simulation_name": simulation_name,
|
133
|
+
"agent_name": agent_name,
|
134
|
+
"risk_name": risk_name,
|
135
|
+
},
|
136
|
+
)
|
137
|
+
response = await risk_evaluation_fn(risk_evaluation_request)
|
138
|
+
return response
|
139
|
+
except Exception as e:
|
140
|
+
raise e
|
141
|
+
|
142
|
+
return risk_evaluation_fn_wrapper
|
143
|
+
else:
|
144
|
+
return risk_evaluation_fn
|
145
|
+
|
146
|
+
return trace_decorator
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: snowglobe
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.9
|
4
4
|
Summary: client server for usage with snowglobe experiments
|
5
5
|
Author-email: Guardrails AI <contact@guardrailsai.com>
|
6
6
|
License: MIT License
|
@@ -119,3 +119,17 @@ def process_scenario(request: CompletionRequest) -> CompletionFunctionOutputs:
|
|
119
119
|
)
|
120
120
|
return CompletionFunctionOutputs(response=response.choices[0].message.content)
|
121
121
|
```
|
122
|
+
|
123
|
+
## Tracing with MLflow
|
124
|
+
The Snowglobe Connect SDK has MLflow tracing built in! Simply `pip install mlflow` and the sdk will take care of the rest. Read more about MLflow's tracing capability for GenAI Apps [here](https://mlflow.org/docs/latest/genai/tracing/app-instrumentation/).
|
125
|
+
|
126
|
+
### Enhancing Snowglobe Connect SDK's Traces with Autologging
|
127
|
+
You can turn on mlflow autologging in your app to add additional context to the traces the Snowglobe Connect SDK captures. In you app's entry point simply call the appropriate autolog method for the LLM provider you're using. The below example shows how to enable this for LiteLLM:
|
128
|
+
```py
|
129
|
+
import mlflow
|
130
|
+
|
131
|
+
mlflow.litellm.autolog()
|
132
|
+
```
|
133
|
+
|
134
|
+
### Disable Snowglobe Connect SDK's MLflow Tracing
|
135
|
+
If you already use MLflow and don't want the Snowglobe Connect SDK to capture additional traces, you can disable this feature by setting the `SNOWGLOBE_DISABLE_MLFLOW_TRACING` environment variable to `true`.
|
@@ -15,6 +15,7 @@ src/snowglobe/client/src/config.py
|
|
15
15
|
src/snowglobe/client/src/models.py
|
16
16
|
src/snowglobe/client/src/project_manager.py
|
17
17
|
src/snowglobe/client/src/stats.py
|
18
|
+
src/snowglobe/client/src/telemetry.py
|
18
19
|
src/snowglobe/client/src/utils.py
|
19
20
|
tests/test_app.py
|
20
21
|
tests/test_cli.py
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|