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
@@ -109,6 +110,7 @@ def __init__(
109110 is_agentless : bool ,
110111 _site : str = "" ,
111112 _api_key : str = "" ,
113+ _app_key : str = "" ,
112114 _override_url : str = "" ,
113115 ) -> None :
114116 super (BaseLLMObsWriter , self ).__init__ (interval = interval )
@@ -118,6 +120,7 @@ def __init__(
118120 self ._timeout : float = timeout
119121 self ._api_key : str = _api_key or config ._dd_api_key
120122 self ._site : str = _site or config ._dd_site
123+ self ._app_key : str = _app_key or config ._dd_app_key
121124 self ._override_url : str = _override_url
122125
123126 self ._agentless : bool = is_agentless
@@ -263,6 +266,71 @@ def _data(self, events: List[LLMObsEvaluationMetricEvent]) -> Dict[str, Any]:
263266 return {"data" : {"type" : "evaluation_metric" , "attributes" : {"metrics" : events }}}
264267
265268
269+ class LLMObsExperimentsClient (BaseLLMObsWriter ):
270+
271+ def request (self , method : str , path : str , body : bytes = b"" ) -> None :
272+ headers = {
273+ "Content-Type" : "application/json" ,
274+ "DD-API-KEY" : self ._api_key ,
275+ "DD-APPLICATION-KEY" : self ._app_key ,
276+ }
277+ site = self ._site
278+ if site == "datad0g.com" :
279+ base = "https://dd.datad0g.com"
280+ else :
281+ base = f"https://api.{ site } "
282+
283+ conn = get_connection (base + path )
284+ try :
285+ conn .request (method , base , body , headers )
286+ resp = conn .getresponse ()
287+ return Response .from_http_response (resp )
288+ finally :
289+ conn .close ()
290+
291+ def dataset_pull (self , name : str ) -> None :
292+ from ddtrace .llmobs ._experiment import Dataset
293+ from ddtrace .llmobs ._experiment import DatasetRecord
294+
295+ path = f"/api/unstable/llm-obs/v1/datasets?filter[name]={ quote (name )} "
296+ resp = self .request ("GET" , path )
297+
298+ response_data = resp .get_json ()
299+ datasets = response_data .get ("data" , [])
300+
301+ if not datasets :
302+ raise ValueError (f"Dataset '{ name } ' not found" )
303+
304+ dataset_id = datasets [0 ]["id" ]
305+
306+ url = f"/api/unstable/llm-obs/v1/datasets/{ dataset_id } /records"
307+ try :
308+ resp = self .request ("GET" , url )
309+ records_data = resp .get_json ()
310+ except ValueError as e :
311+ if "404" in str (e ):
312+ raise ValueError (f"Dataset '{ name } ' not found" ) from e
313+ raise
314+
315+ # Transform records into the expected format
316+ class_records : List [DatasetRecord ] = []
317+ for record in records_data .get ("data" , []):
318+ attrs = record .get ("attributes" , {})
319+ input_data = attrs .get ("input" )
320+ expected_output = attrs .get ("expected_output" )
321+
322+ class_records .append (
323+ {
324+ "record_id" : record .get ("id" ),
325+ "input" : input_data ,
326+ "expected_output" : expected_output ,
327+ ** attrs .get ("metadata" , {}),
328+ }
329+ )
330+
331+ return Dataset (name , class_records )
332+
333+
266334class LLMObsSpanWriter (BaseLLMObsWriter ):
267335 """Writes span events to the LLMObs Span Endpoint."""
268336
0 commit comments