humalab 0.0.5__py3-none-any.whl → 0.0.7__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.
Potentially problematic release.
This version of humalab might be problematic. Click here for more details.
- humalab/__init__.py +25 -0
- humalab/assets/__init__.py +8 -2
- humalab/assets/files/resource_file.py +96 -6
- humalab/assets/files/urdf_file.py +49 -11
- humalab/assets/resource_operator.py +139 -0
- humalab/constants.py +48 -5
- humalab/dists/__init__.py +7 -0
- humalab/dists/bernoulli.py +26 -1
- humalab/dists/categorical.py +25 -0
- humalab/dists/discrete.py +27 -2
- humalab/dists/distribution.py +11 -0
- humalab/dists/gaussian.py +27 -2
- humalab/dists/log_uniform.py +29 -3
- humalab/dists/truncated_gaussian.py +33 -4
- humalab/dists/uniform.py +24 -0
- humalab/episode.py +291 -11
- humalab/humalab.py +93 -38
- humalab/humalab_api_client.py +297 -95
- humalab/humalab_config.py +49 -0
- humalab/humalab_test.py +46 -17
- humalab/metrics/__init__.py +11 -5
- humalab/metrics/code.py +59 -0
- humalab/metrics/metric.py +69 -102
- humalab/metrics/scenario_stats.py +163 -0
- humalab/metrics/summary.py +45 -24
- humalab/run.py +224 -101
- humalab/scenarios/__init__.py +11 -0
- humalab/{scenario.py → scenarios/scenario.py} +130 -136
- humalab/scenarios/scenario_operator.py +114 -0
- humalab/{scenario_test.py → scenarios/scenario_test.py} +150 -269
- humalab/utils.py +37 -0
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/METADATA +1 -1
- humalab-0.0.7.dist-info/RECORD +39 -0
- humalab/assets/resource_manager.py +0 -58
- humalab/evaluators/__init__.py +0 -16
- humalab/humalab_main.py +0 -119
- humalab/metrics/dist_metric.py +0 -22
- humalab-0.0.5.dist-info/RECORD +0 -37
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/WHEEL +0 -0
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/entry_points.txt +0 -0
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/licenses/LICENSE +0 -0
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/top_level.txt +0 -0
humalab/humalab_api_client.py
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
"""HTTP client for accessing HumaLab service APIs with API key authentication."""
|
|
2
2
|
|
|
3
|
+
from enum import Enum
|
|
3
4
|
import os
|
|
4
5
|
import requests
|
|
5
6
|
from typing import Dict, Any, Optional, List
|
|
6
7
|
from urllib.parse import urljoin
|
|
8
|
+
from humalab.humalab_config import HumalabConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RunStatus(Enum):
|
|
12
|
+
"""Status of runs"""
|
|
13
|
+
RUNNING = "running"
|
|
14
|
+
CANCELED = "canceled"
|
|
15
|
+
ERRORED = "errored"
|
|
16
|
+
FINISHED = "finished"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EpisodeStatus(Enum):
|
|
20
|
+
"""Status of validation episodes"""
|
|
21
|
+
RUNNING = "running"
|
|
22
|
+
CANCELED = "canceled"
|
|
23
|
+
ERRORED = "errored"
|
|
24
|
+
SUCCESS = "success"
|
|
25
|
+
FAILED = "failed"
|
|
7
26
|
|
|
8
27
|
|
|
9
28
|
class HumaLabApiClient:
|
|
@@ -19,13 +38,14 @@ class HumaLabApiClient:
|
|
|
19
38
|
Initialize the HumaLab API client.
|
|
20
39
|
|
|
21
40
|
Args:
|
|
22
|
-
base_url: Base URL for the HumaLab service (defaults to
|
|
41
|
+
base_url: Base URL for the HumaLab service (defaults to https://api.humalab.ai)
|
|
23
42
|
api_key: API key for authentication (defaults to HUMALAB_API_KEY env var)
|
|
24
43
|
timeout: Request timeout in seconds
|
|
25
44
|
"""
|
|
26
|
-
|
|
27
|
-
self.
|
|
28
|
-
self.
|
|
45
|
+
humalab_config = HumalabConfig()
|
|
46
|
+
self.base_url = base_url or humalab_config.base_url or os.getenv("HUMALAB_SERVICE_URL", "https://api.humalab.ai")
|
|
47
|
+
self.api_key = api_key or humalab_config.api_key or os.getenv("HUMALAB_API_KEY")
|
|
48
|
+
self.timeout = timeout or humalab_config.timeout or 30.0 # Default timeout of 30 seconds
|
|
29
49
|
|
|
30
50
|
# Ensure base_url ends without trailing slash
|
|
31
51
|
self.base_url = self.base_url.rstrip('/')
|
|
@@ -77,16 +97,35 @@ class HumaLabApiClient:
|
|
|
77
97
|
if files:
|
|
78
98
|
headers.pop("Content-Type", None)
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
headers
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
# Determine if we should send form data or JSON
|
|
101
|
+
# Form data endpoints: /artifacts/code, /artifacts/blob/upload, /artifacts/python
|
|
102
|
+
is_form_endpoint = any(form_path in endpoint for form_path in ['/artifacts/code', '/artifacts/blob', '/artifacts/python'])
|
|
103
|
+
|
|
104
|
+
if is_form_endpoint or files:
|
|
105
|
+
# Send as form data
|
|
106
|
+
headers.pop("Content-Type", None) # Let requests set multipart/form-data
|
|
107
|
+
response = requests.request(
|
|
108
|
+
method=method,
|
|
109
|
+
url=url,
|
|
110
|
+
data=data,
|
|
111
|
+
params=params,
|
|
112
|
+
files=files,
|
|
113
|
+
headers=headers,
|
|
114
|
+
timeout=self.timeout,
|
|
115
|
+
**kwargs
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
# Send as JSON (default behavior)
|
|
119
|
+
response = requests.request(
|
|
120
|
+
method=method,
|
|
121
|
+
url=url,
|
|
122
|
+
json=data,
|
|
123
|
+
params=params,
|
|
124
|
+
files=files,
|
|
125
|
+
headers=headers,
|
|
126
|
+
timeout=self.timeout,
|
|
127
|
+
**kwargs
|
|
128
|
+
)
|
|
90
129
|
|
|
91
130
|
# Raise an exception for HTTP error responses
|
|
92
131
|
response.raise_for_status()
|
|
@@ -123,7 +162,7 @@ class HumaLabApiClient:
|
|
|
123
162
|
Returns:
|
|
124
163
|
User information from the validated token
|
|
125
164
|
"""
|
|
126
|
-
response = self.get("/
|
|
165
|
+
response = self.get("/auth/validate")
|
|
127
166
|
return response.json()
|
|
128
167
|
|
|
129
168
|
# Convenience methods for common API operations
|
|
@@ -134,7 +173,7 @@ class HumaLabApiClient:
|
|
|
134
173
|
resource_types: Optional[str] = None,
|
|
135
174
|
limit: int = 20,
|
|
136
175
|
offset: int = 0,
|
|
137
|
-
latest_only: bool =
|
|
176
|
+
latest_only: bool = True
|
|
138
177
|
) -> Dict[str, Any]:
|
|
139
178
|
"""
|
|
140
179
|
Get list of all resources.
|
|
@@ -258,8 +297,8 @@ class HumaLabApiClient:
|
|
|
258
297
|
def get_scenarios(
|
|
259
298
|
self,
|
|
260
299
|
project_name: str,
|
|
261
|
-
|
|
262
|
-
|
|
300
|
+
limit: int = 20,
|
|
301
|
+
offset: int = 0,
|
|
263
302
|
include_inactive: bool = False,
|
|
264
303
|
search: Optional[str] = None,
|
|
265
304
|
status_filter: Optional[str] = None
|
|
@@ -269,8 +308,8 @@ class HumaLabApiClient:
|
|
|
269
308
|
|
|
270
309
|
Args:
|
|
271
310
|
project_name: Project name (required)
|
|
272
|
-
skip: Number of scenarios to skip for pagination
|
|
273
311
|
limit: Maximum number of scenarios to return (1-100)
|
|
312
|
+
offset: Number of scenarios to skip
|
|
274
313
|
include_inactive: Include inactive scenarios in results
|
|
275
314
|
search: Search term to filter by name, description, or UUID
|
|
276
315
|
status_filter: Filter by specific status
|
|
@@ -280,7 +319,7 @@ class HumaLabApiClient:
|
|
|
280
319
|
"""
|
|
281
320
|
params = {
|
|
282
321
|
"project_name": project_name,
|
|
283
|
-
"skip":
|
|
322
|
+
"skip": offset,
|
|
284
323
|
"limit": limit,
|
|
285
324
|
"include_inactive": include_inactive
|
|
286
325
|
}
|
|
@@ -441,7 +480,8 @@ class HumaLabApiClient:
|
|
|
441
480
|
"name": name,
|
|
442
481
|
"project_name": project_name,
|
|
443
482
|
"arguments": arguments or [],
|
|
444
|
-
"tags": tags or []
|
|
483
|
+
"tags": tags or [],
|
|
484
|
+
"status": RunStatus.RUNNING.value
|
|
445
485
|
}
|
|
446
486
|
if description:
|
|
447
487
|
data["description"] = description
|
|
@@ -452,7 +492,7 @@ class HumaLabApiClient:
|
|
|
452
492
|
def get_runs(
|
|
453
493
|
self,
|
|
454
494
|
project_name: Optional[str],
|
|
455
|
-
status: Optional[
|
|
495
|
+
status: Optional[RunStatus] = None,
|
|
456
496
|
tags: Optional[List[str]] = None,
|
|
457
497
|
limit: int = 20,
|
|
458
498
|
offset: int = 0
|
|
@@ -475,7 +515,7 @@ class HumaLabApiClient:
|
|
|
475
515
|
raise ValueError("project_name is required to get runs.")
|
|
476
516
|
params["project_name"] = project_name
|
|
477
517
|
if status:
|
|
478
|
-
params["status"] = status
|
|
518
|
+
params["status"] = status.value
|
|
479
519
|
if tags:
|
|
480
520
|
params["tags"] = ",".join(tags)
|
|
481
521
|
|
|
@@ -500,7 +540,8 @@ class HumaLabApiClient:
|
|
|
500
540
|
run_id: str,
|
|
501
541
|
name: Optional[str] = None,
|
|
502
542
|
description: Optional[str] = None,
|
|
503
|
-
status: Optional[
|
|
543
|
+
status: Optional[RunStatus] = None,
|
|
544
|
+
err_msg: Optional[str] = None,
|
|
504
545
|
arguments: Optional[List[Dict[str, str]]] = None,
|
|
505
546
|
tags: Optional[List[str]] = None
|
|
506
547
|
) -> Dict[str, Any]:
|
|
@@ -512,6 +553,7 @@ class HumaLabApiClient:
|
|
|
512
553
|
name: Optional new name
|
|
513
554
|
description: Optional new description
|
|
514
555
|
status: Optional new status
|
|
556
|
+
err_msg: Optional error message
|
|
515
557
|
arguments: Optional new arguments
|
|
516
558
|
tags: Optional new tags
|
|
517
559
|
|
|
@@ -524,7 +566,9 @@ class HumaLabApiClient:
|
|
|
524
566
|
if description is not None:
|
|
525
567
|
data["description"] = description
|
|
526
568
|
if status is not None:
|
|
527
|
-
data["status"] = status
|
|
569
|
+
data["status"] = status.value
|
|
570
|
+
if err_msg is not None:
|
|
571
|
+
data["err_msg"] = err_msg
|
|
528
572
|
if arguments is not None:
|
|
529
573
|
data["arguments"] = arguments
|
|
530
574
|
if tags is not None:
|
|
@@ -536,26 +580,26 @@ class HumaLabApiClient:
|
|
|
536
580
|
def create_episode(
|
|
537
581
|
self,
|
|
538
582
|
run_id: str,
|
|
539
|
-
|
|
540
|
-
status: Optional[
|
|
583
|
+
episode_id: str,
|
|
584
|
+
status: Optional[EpisodeStatus] = None
|
|
541
585
|
) -> Dict[str, Any]:
|
|
542
586
|
"""
|
|
543
587
|
Create a new episode.
|
|
544
588
|
|
|
545
589
|
Args:
|
|
546
590
|
run_id: Run ID
|
|
547
|
-
|
|
591
|
+
episode_id: Episode name
|
|
548
592
|
status: Optional episode status
|
|
549
593
|
|
|
550
594
|
Returns:
|
|
551
595
|
Created episode data
|
|
552
596
|
"""
|
|
553
597
|
data = {
|
|
554
|
-
"
|
|
598
|
+
"episode_id": episode_id,
|
|
555
599
|
"run_id": run_id
|
|
556
600
|
}
|
|
557
601
|
if status:
|
|
558
|
-
data["status"] = status
|
|
602
|
+
data["status"] = status.value
|
|
559
603
|
|
|
560
604
|
response = self.post("/episodes", data=data)
|
|
561
605
|
return response.json()
|
|
@@ -563,7 +607,7 @@ class HumaLabApiClient:
|
|
|
563
607
|
def get_episodes(
|
|
564
608
|
self,
|
|
565
609
|
run_id: Optional[str] = None,
|
|
566
|
-
status: Optional[
|
|
610
|
+
status: Optional[EpisodeStatus] = None,
|
|
567
611
|
limit: int = 20,
|
|
568
612
|
offset: int = 0
|
|
569
613
|
) -> Dict[str, Any]:
|
|
@@ -583,182 +627,340 @@ class HumaLabApiClient:
|
|
|
583
627
|
if run_id:
|
|
584
628
|
params["run_id"] = run_id
|
|
585
629
|
if status:
|
|
586
|
-
params["status"] = status
|
|
630
|
+
params["status"] = status.value
|
|
587
631
|
|
|
588
632
|
response = self.get("/episodes", params=params)
|
|
589
633
|
return response.json()
|
|
590
634
|
|
|
591
|
-
def get_episode(self, run_id: str,
|
|
635
|
+
def get_episode(self, run_id: str, episode_id: str) -> Dict[str, Any]:
|
|
592
636
|
"""
|
|
593
637
|
Get a specific episode.
|
|
594
638
|
|
|
595
639
|
Args:
|
|
596
640
|
run_id: Run ID
|
|
597
|
-
|
|
641
|
+
episode_id: Episode name
|
|
598
642
|
|
|
599
643
|
Returns:
|
|
600
644
|
Episode data
|
|
601
645
|
"""
|
|
602
|
-
response = self.get(f"/episodes/{run_id}/{
|
|
646
|
+
response = self.get(f"/episodes/{run_id}/{episode_id}")
|
|
603
647
|
return response.json()
|
|
604
648
|
|
|
605
649
|
def update_episode(
|
|
606
650
|
self,
|
|
607
651
|
run_id: str,
|
|
608
|
-
|
|
609
|
-
status: Optional[
|
|
652
|
+
episode_id: str,
|
|
653
|
+
status: Optional[EpisodeStatus] = None,
|
|
654
|
+
err_msg: Optional[str] = None
|
|
610
655
|
) -> Dict[str, Any]:
|
|
611
656
|
"""
|
|
612
657
|
Update an episode.
|
|
613
658
|
|
|
614
659
|
Args:
|
|
615
660
|
run_id: Run ID
|
|
616
|
-
|
|
661
|
+
episode_id: Episode name
|
|
617
662
|
status: Optional new status
|
|
618
|
-
|
|
663
|
+
err_msg: Optional error message
|
|
664
|
+
|
|
619
665
|
Returns:
|
|
620
666
|
Updated episode data
|
|
621
667
|
"""
|
|
622
668
|
data = {}
|
|
623
669
|
if status is not None:
|
|
624
|
-
data["status"] = status
|
|
625
|
-
|
|
626
|
-
|
|
670
|
+
data["status"] = status.value
|
|
671
|
+
if err_msg is not None:
|
|
672
|
+
data["err_msg"] = err_msg
|
|
673
|
+
response = self.put(f"/episodes/{run_id}/{episode_id}", data=data)
|
|
627
674
|
return response.json()
|
|
628
675
|
|
|
629
|
-
def delete_episode(self, run_id: str,
|
|
676
|
+
def delete_episode(self, run_id: str, episode_id: str) -> None:
|
|
630
677
|
"""
|
|
631
678
|
Delete an episode.
|
|
632
679
|
|
|
633
680
|
Args:
|
|
634
681
|
run_id: Run ID
|
|
635
|
-
|
|
682
|
+
episode_id: Episode name
|
|
636
683
|
"""
|
|
637
|
-
self.delete(f"/episodes/{run_id}/{
|
|
684
|
+
self.delete(f"/episodes/{run_id}/{episode_id}")
|
|
638
685
|
|
|
639
686
|
def upload_blob(
|
|
640
687
|
self,
|
|
641
688
|
artifact_key: str,
|
|
642
689
|
run_id: str,
|
|
643
|
-
file_path: str,
|
|
644
690
|
artifact_type: str,
|
|
645
|
-
|
|
646
|
-
|
|
691
|
+
file_content: bytes | None = None,
|
|
692
|
+
file_path: str | None = None,
|
|
693
|
+
episode_id: Optional[str] = None,
|
|
694
|
+
filename: Optional[str] = None,
|
|
695
|
+
content_type: Optional[str] = None
|
|
647
696
|
) -> Dict[str, Any]:
|
|
648
697
|
"""
|
|
649
698
|
Upload a blob artifact (image/video).
|
|
650
|
-
|
|
699
|
+
|
|
651
700
|
Args:
|
|
652
701
|
artifact_key: Artifact key identifier
|
|
653
702
|
run_id: Run ID
|
|
654
|
-
file_path: Path to file to upload
|
|
655
703
|
artifact_type: Type of artifact ('image' or 'video')
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
704
|
+
file_content: File content as bytes
|
|
705
|
+
file_path: Path to file to upload
|
|
706
|
+
episode_id: Optional episode ID (None for run-level artifacts)
|
|
707
|
+
filename: Optional filename to use for the uploaded file
|
|
708
|
+
content_type: Optional content type (e.g., 'image/png', 'video/mp4')
|
|
709
|
+
|
|
659
710
|
Returns:
|
|
660
711
|
Created artifact data
|
|
661
712
|
"""
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
713
|
+
form_data = {
|
|
714
|
+
'artifact_key': artifact_key,
|
|
715
|
+
'run_id': run_id,
|
|
716
|
+
'artifact_type': artifact_type
|
|
717
|
+
}
|
|
718
|
+
if episode_id:
|
|
719
|
+
form_data['episode_id'] = episode_id
|
|
720
|
+
if filename:
|
|
721
|
+
form_data['filename'] = filename
|
|
722
|
+
if content_type:
|
|
723
|
+
form_data['content_type'] = content_type
|
|
724
|
+
|
|
725
|
+
if file_path:
|
|
726
|
+
with open(file_path, 'rb') as f:
|
|
727
|
+
files = {'file': f}
|
|
728
|
+
response = self.post("/artifacts/blob/upload", files=files, data=form_data)
|
|
729
|
+
elif file_content:
|
|
730
|
+
files = {'file': ('blob', file_content)}
|
|
674
731
|
response = self.post("/artifacts/blob/upload", files=files, data=form_data)
|
|
675
|
-
|
|
732
|
+
else:
|
|
733
|
+
raise ValueError("Either file_path or file_content must be provided for blob upload.")
|
|
734
|
+
return response.json()
|
|
676
735
|
|
|
677
736
|
def upsert_metrics(
|
|
678
737
|
self,
|
|
679
738
|
artifact_key: str,
|
|
680
739
|
run_id: str,
|
|
681
740
|
metric_type: str,
|
|
682
|
-
metric_data: List[Dict[str, Any]],
|
|
683
|
-
|
|
684
|
-
description: Optional[str] = None
|
|
741
|
+
metric_data: Optional[List[Dict[str, Any]]] = None,
|
|
742
|
+
episode_id: Optional[str] = None
|
|
685
743
|
) -> Dict[str, Any]:
|
|
686
744
|
"""
|
|
687
745
|
Upsert metrics artifact (create or append).
|
|
688
|
-
|
|
746
|
+
|
|
689
747
|
Args:
|
|
690
748
|
artifact_key: Artifact key identifier
|
|
691
749
|
run_id: Run ID
|
|
692
750
|
metric_type: Type of metric display ('line', 'bar', 'scatter', 'gauge', 'counter')
|
|
693
751
|
metric_data: List of metric data points with 'key', 'values', 'timestamp'
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
752
|
+
episode_id: Optional episode ID (None for run-level artifacts)
|
|
753
|
+
|
|
697
754
|
Returns:
|
|
698
755
|
Created/updated artifact data
|
|
699
756
|
"""
|
|
700
757
|
data = {
|
|
701
758
|
"artifact_key": artifact_key,
|
|
702
759
|
"run_id": run_id,
|
|
703
|
-
"metric_type": metric_type
|
|
704
|
-
"metric_data": metric_data
|
|
760
|
+
"metric_type": metric_type
|
|
705
761
|
}
|
|
706
|
-
if
|
|
707
|
-
data["
|
|
708
|
-
if
|
|
709
|
-
data["
|
|
710
|
-
|
|
762
|
+
if episode_id:
|
|
763
|
+
data["episode_id"] = episode_id
|
|
764
|
+
if metric_data:
|
|
765
|
+
data["metric_data"] = metric_data
|
|
766
|
+
|
|
711
767
|
response = self.post("/artifacts/metrics", data=data)
|
|
712
768
|
return response.json()
|
|
713
769
|
|
|
714
770
|
def get_artifacts(
|
|
715
771
|
self,
|
|
716
772
|
run_id: Optional[str] = None,
|
|
717
|
-
|
|
773
|
+
episode_id: Optional[str] = None,
|
|
718
774
|
artifact_type: Optional[str] = None,
|
|
719
775
|
limit: int = 20,
|
|
720
776
|
offset: int = 0
|
|
721
777
|
) -> Dict[str, Any]:
|
|
722
778
|
"""
|
|
723
779
|
Get list of artifacts.
|
|
724
|
-
|
|
780
|
+
|
|
725
781
|
Args:
|
|
726
782
|
run_id: Filter by run ID
|
|
727
|
-
|
|
783
|
+
episode_id: Filter by episode ID
|
|
728
784
|
artifact_type: Filter by artifact type
|
|
729
|
-
limit: Maximum number of artifacts to return
|
|
785
|
+
limit: Maximum number of artifacts to return (0 for no limit)
|
|
730
786
|
offset: Number of artifacts to skip
|
|
731
|
-
|
|
787
|
+
|
|
732
788
|
Returns:
|
|
733
789
|
Artifact list with pagination info
|
|
734
790
|
"""
|
|
735
791
|
params = {"limit": limit, "offset": offset}
|
|
736
792
|
if run_id:
|
|
737
793
|
params["run_id"] = run_id
|
|
738
|
-
if
|
|
739
|
-
params["
|
|
794
|
+
if episode_id:
|
|
795
|
+
params["episode_id"] = episode_id
|
|
740
796
|
if artifact_type:
|
|
741
797
|
params["artifact_type"] = artifact_type
|
|
742
|
-
|
|
798
|
+
|
|
743
799
|
response = self.get("/artifacts", params=params)
|
|
744
800
|
return response.json()
|
|
745
801
|
|
|
746
802
|
def get_artifact(
|
|
747
|
-
self,
|
|
748
|
-
run_id: str,
|
|
749
|
-
|
|
803
|
+
self,
|
|
804
|
+
run_id: str,
|
|
805
|
+
episode_id: str,
|
|
750
806
|
artifact_key: str
|
|
751
807
|
) -> Dict[str, Any]:
|
|
752
808
|
"""
|
|
753
809
|
Get a specific artifact.
|
|
754
|
-
|
|
810
|
+
|
|
755
811
|
Args:
|
|
756
812
|
run_id: Run ID
|
|
757
|
-
|
|
813
|
+
episode_id: Episode ID
|
|
758
814
|
artifact_key: Artifact key
|
|
759
|
-
|
|
815
|
+
|
|
760
816
|
Returns:
|
|
761
817
|
Artifact data
|
|
762
818
|
"""
|
|
763
|
-
response = self.get(f"/artifacts/{run_id}/{
|
|
819
|
+
response = self.get(f"/artifacts/{run_id}/{episode_id}/{artifact_key}")
|
|
820
|
+
return response.json()
|
|
821
|
+
|
|
822
|
+
def upload_code(
|
|
823
|
+
self,
|
|
824
|
+
artifact_key: str,
|
|
825
|
+
run_id: str,
|
|
826
|
+
code_content: str,
|
|
827
|
+
episode_id: Optional[str] = None
|
|
828
|
+
) -> Dict[str, Any]:
|
|
829
|
+
"""
|
|
830
|
+
Upload code artifact (YAML/string content).
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
artifact_key: Artifact key identifier
|
|
834
|
+
run_id: Run ID
|
|
835
|
+
code_content: Code/text content to upload
|
|
836
|
+
episode_id: Optional episode ID (None for run-level artifacts)
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
Created artifact data
|
|
840
|
+
"""
|
|
841
|
+
data = {
|
|
842
|
+
'artifact_key': artifact_key,
|
|
843
|
+
'run_id': run_id,
|
|
844
|
+
'code_content': code_content
|
|
845
|
+
}
|
|
846
|
+
if episode_id:
|
|
847
|
+
data['episode_id'] = episode_id
|
|
848
|
+
|
|
849
|
+
response = self.post("/artifacts/code", data=data)
|
|
850
|
+
return response.json()
|
|
851
|
+
|
|
852
|
+
def upload_python(
|
|
853
|
+
self,
|
|
854
|
+
artifact_key: str,
|
|
855
|
+
run_id: str,
|
|
856
|
+
pickled_bytes: bytes,
|
|
857
|
+
episode_id: Optional[str] = None
|
|
858
|
+
) -> Dict[str, Any]:
|
|
859
|
+
"""
|
|
860
|
+
Upload pickled Python object as artifact.
|
|
861
|
+
|
|
862
|
+
Args:
|
|
863
|
+
artifact_key: Artifact key identifier
|
|
864
|
+
run_id: Run ID
|
|
865
|
+
pickled_bytes: Pickled Python object as bytes
|
|
866
|
+
episode_id: Optional episode ID (None for run-level artifacts)
|
|
867
|
+
|
|
868
|
+
Returns:
|
|
869
|
+
Created artifact data
|
|
870
|
+
"""
|
|
871
|
+
data = {
|
|
872
|
+
'artifact_key': artifact_key,
|
|
873
|
+
'run_id': run_id
|
|
874
|
+
}
|
|
875
|
+
if episode_id:
|
|
876
|
+
data['episode_id'] = episode_id
|
|
877
|
+
|
|
878
|
+
files = {'file': pickled_bytes}
|
|
879
|
+
response = self.post("/artifacts/python", files=files, data=data)
|
|
880
|
+
return response.json()
|
|
881
|
+
|
|
882
|
+
def upload_scenario_stats_artifact(
|
|
883
|
+
self,
|
|
884
|
+
artifact_key: str,
|
|
885
|
+
run_id: str,
|
|
886
|
+
pickled_bytes: bytes,
|
|
887
|
+
graph_type: str,
|
|
888
|
+
) -> Dict[str, Any]:
|
|
889
|
+
"""
|
|
890
|
+
Upload scenario stats artifact (pickled Python dict data).
|
|
891
|
+
This is an upsert operation - creates if doesn't exist, appends if it does.
|
|
892
|
+
Run-level only (no episode_id support).
|
|
893
|
+
|
|
894
|
+
Args:
|
|
895
|
+
artifact_key: Artifact key identifier
|
|
896
|
+
run_id: Run ID
|
|
897
|
+
pickled_bytes: Pickled Python dict as bytes containing scenario stats
|
|
898
|
+
graph_type: Graph display type - one of: 'line', 'bar', 'scatter',
|
|
899
|
+
'histogram', 'gaussian', 'heatmap', '3d_map'
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
Created/updated artifact data
|
|
903
|
+
"""
|
|
904
|
+
data = {
|
|
905
|
+
'artifact_key': artifact_key,
|
|
906
|
+
'run_id': run_id,
|
|
907
|
+
'graph_type': graph_type
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
files = {'file': pickled_bytes}
|
|
911
|
+
response = self.post("/artifacts/scenario_stats", files=files, data=data)
|
|
912
|
+
return response.json()
|
|
913
|
+
|
|
914
|
+
def download_artifact(
|
|
915
|
+
self,
|
|
916
|
+
run_id: str,
|
|
917
|
+
episode_id: str,
|
|
918
|
+
artifact_key: str
|
|
919
|
+
) -> bytes:
|
|
920
|
+
"""
|
|
921
|
+
Download a blob artifact file.
|
|
922
|
+
|
|
923
|
+
Args:
|
|
924
|
+
run_id: Run ID
|
|
925
|
+
episode_id: Episode ID
|
|
926
|
+
artifact_key: Artifact key
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
Artifact file content as bytes
|
|
930
|
+
"""
|
|
931
|
+
endpoint = f"/artifacts/{run_id}/{episode_id}/{artifact_key}/download"
|
|
932
|
+
response = self.get(endpoint)
|
|
933
|
+
return response.content
|
|
934
|
+
|
|
935
|
+
def upload_metrics(
|
|
936
|
+
self,
|
|
937
|
+
run_id: str,
|
|
938
|
+
artifact_key: str,
|
|
939
|
+
pickled_bytes: bytes,
|
|
940
|
+
graph_type: str,
|
|
941
|
+
episode_id: str | None = None,
|
|
942
|
+
) -> Dict[str, Any]:
|
|
943
|
+
"""
|
|
944
|
+
Upload metrics artifact.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
run_id: Run ID
|
|
948
|
+
artifact_key: Artifact key
|
|
949
|
+
pickled_bytes: Pickled metrics data as bytes
|
|
950
|
+
graph_type: Optional new graph type
|
|
951
|
+
episode_id: Optional new episode ID
|
|
952
|
+
|
|
953
|
+
Returns:
|
|
954
|
+
Updated artifact data
|
|
955
|
+
"""
|
|
956
|
+
data = {
|
|
957
|
+
"run_id": run_id,
|
|
958
|
+
"artifact_key": artifact_key,
|
|
959
|
+
'graph_type': graph_type
|
|
960
|
+
}
|
|
961
|
+
files = {'file': pickled_bytes}
|
|
962
|
+
if episode_id:
|
|
963
|
+
data["episode_id"] = episode_id
|
|
964
|
+
|
|
965
|
+
response = self.post("/artifacts/metrics", files=files, data=data)
|
|
764
966
|
return response.json()
|