44from typing import List
55from typing import Optional
66from typing import Union
7+ from urllib .parse import quote
78
89
910# TypedDict was added to typing in python 3.8
3536from ddtrace .llmobs ._constants import EVP_SUBDOMAIN_HEADER_NAME
3637from ddtrace .llmobs ._constants import SPAN_ENDPOINT
3738from ddtrace .llmobs ._constants import SPAN_SUBDOMAIN_NAME
39+ from ddtrace .llmobs ._experiment import Dataset
40+ from ddtrace .llmobs ._experiment import DatasetRecord
3841from ddtrace .llmobs ._utils import safe_json
3942from ddtrace .settings ._agent import config as agent_config
4043
@@ -109,6 +112,7 @@ def __init__(
109112 is_agentless : bool ,
110113 _site : str = "" ,
111114 _api_key : str = "" ,
115+ _app_key : str = "" ,
112116 _override_url : str = "" ,
113117 ) -> None :
114118 super (BaseLLMObsWriter , self ).__init__ (interval = interval )
@@ -118,6 +122,7 @@ def __init__(
118122 self ._timeout : float = timeout
119123 self ._api_key : str = _api_key or config ._dd_api_key
120124 self ._site : str = _site or config ._dd_site
125+ self ._app_key : str = _app_key or config ._dd_app_key
121126 self ._override_url : str = _override_url
122127
123128 self ._agentless : bool = is_agentless
@@ -263,6 +268,71 @@ def _data(self, events: List[LLMObsEvaluationMetricEvent]) -> Dict[str, Any]:
263268 return {"data" : {"type" : "evaluation_metric" , "attributes" : {"metrics" : events }}}
264269
265270
271+ class LLMObsExperimentsClient (BaseLLMObsWriter ):
272+
273+ def request (self , method : str , path : str , body : bytes = b"" ) -> Response :
274+ headers = {
275+ "Content-Type" : "application/json" ,
276+ "DD-API-KEY" : self ._api_key ,
277+ "DD-APPLICATION-KEY" : self ._app_key ,
278+ }
279+ site = self ._site
280+ if site == "datad0g.com" :
281+ base = "https://dd.datad0g.com"
282+ else :
283+ base = f"https://api.{ site } "
284+
285+ conn = get_connection (base )
286+ try :
287+ url = base + path
288+ logger .debug ("requesting %s" , url )
289+ conn .request (method , url , body , headers )
290+ resp = conn .getresponse ()
291+ if resp .status >= 300 :
292+ raise ValueError (f"Failed to { method } { path } : { resp .status } " )
293+ return Response .from_http_response (resp )
294+ finally :
295+ conn .close ()
296+
297+ def dataset_pull (self , name : str ) -> Dataset :
298+
299+ path = f"/api/unstable/llm-obs/v1/datasets?filter[name]={ quote (name )} "
300+ resp = self .request ("GET" , path )
301+
302+ response_data = resp .get_json ()
303+ datasets = response_data .get ("data" , [])
304+
305+ if not datasets :
306+ raise ValueError (f"Dataset '{ name } ' not found" )
307+
308+ dataset_id = datasets [0 ]["id" ]
309+ url = f"/api/unstable/llm-obs/v1/datasets/{ dataset_id } /records"
310+ try :
311+ resp = self .request ("GET" , url )
312+ records_data = resp .get_json ()
313+ except ValueError as e :
314+ if "404" in str (e ):
315+ raise ValueError (f"Dataset '{ name } ' not found" ) from e
316+ raise
317+
318+ class_records : List [DatasetRecord ] = []
319+ for record in records_data .get ("data" , []):
320+ attrs = record .get ("attributes" , {})
321+ input_data = attrs .get ("input" )
322+ expected_output = attrs .get ("expected_output" )
323+
324+ class_records .append (
325+ {
326+ "record_id" : record .get ("id" ),
327+ "input" : input_data ,
328+ "expected_output" : expected_output ,
329+ ** attrs .get ("metadata" , {}),
330+ }
331+ )
332+
333+ return Dataset (name , dataset_id , class_records )
334+
335+
266336class LLMObsSpanWriter (BaseLLMObsWriter ):
267337 """Writes span events to the LLMObs Span Endpoint."""
268338
0 commit comments