eval-protocol 0.2.35.dev1__py3-none-any.whl → 0.2.35.dev2__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.
eval_protocol/__init__.py CHANGED
@@ -24,7 +24,7 @@ from .mcp_env import (
24
24
  )
25
25
  from .data_loader import DynamicDataLoader, InlineDataLoader
26
26
  from . import mcp, rewards
27
- from .models import EvaluateResult, Message, MetricResult, EvaluationRow, InputMetadata
27
+ from .models import EvaluateResult, Message, MetricResult, EvaluationRow, InputMetadata, Status
28
28
  from .playback_policy import PlaybackPolicyBase
29
29
  from .resources import create_llm_resource
30
30
  from .reward_function import RewardFunction
@@ -63,6 +63,7 @@ except ImportError:
63
63
  warnings.filterwarnings("default", category=DeprecationWarning, module="eval_protocol")
64
64
 
65
65
  __all__ = [
66
+ "Status",
66
67
  "RemoteRolloutProcessor",
67
68
  "InputMetadata",
68
69
  "EvaluationRow",
eval_protocol/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-10-02T01:54:12-0700",
11
+ "date": "2025-10-02T12:04:07-0700",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "c4108ba1d87ba6fb76100c63e6ee16f48bc06598",
15
- "version": "0.2.35-dev1"
14
+ "full-revisionid": "52178b3b90bb27a7f53fcbbba0bfbb50e7ebb416",
15
+ "version": "0.2.35-dev2"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
eval_protocol/cli.py CHANGED
@@ -15,14 +15,8 @@ from pathlib import Path
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
- from eval_protocol.evaluation import create_evaluation, preview_evaluation
19
-
20
18
  from .cli_commands.agent_eval_cmd import agent_eval_command
21
- from .cli_commands.common import (
22
- check_agent_environment,
23
- check_environment,
24
- setup_logging,
25
- )
19
+ from .cli_commands.common import setup_logging
26
20
  from .cli_commands.deploy import deploy_command
27
21
  from .cli_commands.deploy_mcp import deploy_mcp_command
28
22
  from .cli_commands.logs import logs_command
@@ -27,11 +27,7 @@ import logging # For logger instance
27
27
  import os # For environment variables
28
28
  from pathlib import Path
29
29
 
30
- from pydantic import ValidationError
31
-
32
- from eval_protocol.agent import Orchestrator
33
30
  from eval_protocol.agent.task_manager import TaskManager
34
- from eval_protocol.models import TaskDefinitionModel # Import the new Pydantic model
35
31
 
36
32
  # setup_logging is already called in cli.py's main, but good for standalone use if any
37
33
  # from .common import setup_logging
@@ -17,7 +17,6 @@ from omegaconf import ( # Ensure MISSING is imported if used in configs
17
17
  OmegaConf,
18
18
  )
19
19
 
20
- from eval_protocol.execution.pipeline import EvaluationPipeline
21
20
 
22
21
  logger = logging.getLogger(__name__)
23
22
 
@@ -26,6 +25,8 @@ def run_evaluation_command_logic(cfg: DictConfig) -> None:
26
25
  """
27
26
  Main logic for the 'run-evaluation' command.
28
27
  """
28
+ from eval_protocol.execution.pipeline import EvaluationPipeline
29
+
29
30
  logger.info("Starting 'run-evaluation' command with resolved Hydra config.")
30
31
 
31
32
  # Make Hydra's runtime output directory available to the pipeline if needed
@@ -0,0 +1,286 @@
1
+ """
2
+ Centralized Elasticsearch client for all Elasticsearch API operations.
3
+
4
+ This module provides a unified interface for all Elasticsearch operations
5
+ used throughout the codebase, including index management, document operations,
6
+ and search functionality.
7
+ """
8
+
9
+ import json
10
+ import requests
11
+ from typing import Any, Dict, List, Optional, Union
12
+ from urllib.parse import urlparse
13
+ from eval_protocol.types.remote_rollout_processor import ElasticsearchConfig
14
+
15
+
16
+ class ElasticsearchClient:
17
+ """Centralized client for all Elasticsearch operations."""
18
+
19
+ def __init__(self, config: ElasticsearchConfig):
20
+ """Initialize the Elasticsearch client.
21
+
22
+ Args:
23
+ config: Elasticsearch configuration
24
+ """
25
+ self.config = config
26
+ self.base_url = config.url.rstrip("/")
27
+ self.index_url = f"{self.base_url}/{config.index_name}"
28
+ self._headers = {"Content-Type": "application/json", "Authorization": f"ApiKey {config.api_key}"}
29
+
30
+ def _make_request(
31
+ self,
32
+ method: str,
33
+ url: str,
34
+ json_data: Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] = None,
35
+ params: Optional[Dict[str, Any]] = None,
36
+ timeout: int = 30,
37
+ ) -> requests.Response:
38
+ """Make an HTTP request to Elasticsearch.
39
+
40
+ Args:
41
+ method: HTTP method (GET, POST, PUT, DELETE, HEAD)
42
+ url: Full URL for the request
43
+ json_data: JSON data to send in request body
44
+ params: Query parameters
45
+ timeout: Request timeout in seconds
46
+
47
+ Returns:
48
+ requests.Response object
49
+
50
+ Raises:
51
+ requests.RequestException: If the request fails
52
+ """
53
+ return requests.request(
54
+ method=method,
55
+ url=url,
56
+ headers=self._headers,
57
+ json=json_data,
58
+ params=params,
59
+ verify=self.config.verify_ssl,
60
+ timeout=timeout,
61
+ )
62
+
63
+ # Index Management Operations
64
+
65
+ def create_index(self, mapping: Dict[str, Any]) -> bool:
66
+ """Create an index with the specified mapping.
67
+
68
+ Args:
69
+ mapping: Index mapping configuration
70
+
71
+ Returns:
72
+ bool: True if successful, False otherwise
73
+ """
74
+ try:
75
+ response = self._make_request("PUT", self.index_url, json_data=mapping)
76
+ return response.status_code in [200, 201]
77
+ except Exception:
78
+ return False
79
+
80
+ def index_exists(self) -> bool:
81
+ """Check if the index exists.
82
+
83
+ Returns:
84
+ bool: True if index exists, False otherwise
85
+ """
86
+ try:
87
+ response = self._make_request("HEAD", self.index_url)
88
+ return response.status_code == 200
89
+ except Exception:
90
+ return False
91
+
92
+ def delete_index(self) -> bool:
93
+ """Delete the index.
94
+
95
+ Returns:
96
+ bool: True if successful, False otherwise
97
+ """
98
+ try:
99
+ response = self._make_request("DELETE", self.index_url)
100
+ return response.status_code in [200, 404] # 404 means index doesn't exist
101
+ except Exception:
102
+ return False
103
+
104
+ def get_mapping(self) -> Optional[Dict[str, Any]]:
105
+ """Get the index mapping.
106
+
107
+ Returns:
108
+ Dict containing mapping data, or None if failed
109
+ """
110
+ try:
111
+ response = self._make_request("GET", f"{self.index_url}/_mapping")
112
+ if response.status_code == 200:
113
+ return response.json()
114
+ return None
115
+ except Exception:
116
+ return None
117
+
118
+ def get_index_stats(self) -> Optional[Dict[str, Any]]:
119
+ """Get index statistics.
120
+
121
+ Returns:
122
+ Dict containing index statistics, or None if failed
123
+ """
124
+ try:
125
+ response = self._make_request("GET", f"{self.index_url}/_stats")
126
+ if response.status_code == 200:
127
+ return response.json()
128
+ return None
129
+ except Exception:
130
+ return None
131
+
132
+ # Document Operations
133
+
134
+ def index_document(self, document: Dict[str, Any], doc_id: Optional[str] = None) -> bool:
135
+ """Index a document.
136
+
137
+ Args:
138
+ document: Document to index
139
+ doc_id: Optional document ID
140
+
141
+ Returns:
142
+ bool: True if successful, False otherwise
143
+ """
144
+ try:
145
+ if doc_id:
146
+ url = f"{self.index_url}/_doc/{doc_id}"
147
+ else:
148
+ url = f"{self.index_url}/_doc"
149
+
150
+ response = self._make_request("POST", url, json_data=document)
151
+ return response.status_code in [200, 201]
152
+ except Exception:
153
+ return False
154
+
155
+ def bulk_index_documents(self, documents: List[Dict[str, Any]]) -> bool:
156
+ """Bulk index multiple documents.
157
+
158
+ Args:
159
+ documents: List of documents to index
160
+
161
+ Returns:
162
+ bool: True if successful, False otherwise
163
+ """
164
+ try:
165
+ # Prepare bulk request body
166
+ bulk_body = []
167
+ for doc in documents:
168
+ bulk_body.append({"index": {}})
169
+ bulk_body.append(doc)
170
+
171
+ response = self._make_request("POST", f"{self.index_url}/_bulk", json_data=bulk_body)
172
+ return response.status_code == 200
173
+ except Exception:
174
+ return False
175
+
176
+ # Search Operations
177
+
178
+ def search(
179
+ self, query: Dict[str, Any], size: int = 10, from_: int = 0, sort: Optional[List[Dict[str, Any]]] = None
180
+ ) -> Optional[Dict[str, Any]]:
181
+ """Search documents in the index.
182
+
183
+ Args:
184
+ query: Elasticsearch query
185
+ size: Number of results to return
186
+ from_: Starting offset
187
+ sort: Sort specification
188
+
189
+ Returns:
190
+ Dict containing search results, or None if failed
191
+ """
192
+ try:
193
+ search_body = {"query": query, "size": size, "from": from_}
194
+
195
+ if sort:
196
+ search_body["sort"] = sort
197
+
198
+ response = self._make_request("POST", f"{self.index_url}/_search", json_data=search_body)
199
+
200
+ if response.status_code == 200:
201
+ return response.json()
202
+ return None
203
+ except Exception:
204
+ return None
205
+
206
+ def search_by_term(self, field: str, value: Any, size: int = 10) -> Optional[Dict[str, Any]]:
207
+ """Search documents by exact term match.
208
+
209
+ Args:
210
+ field: Field name to search
211
+ value: Value to match
212
+ size: Number of results to return
213
+
214
+ Returns:
215
+ Dict containing search results, or None if failed
216
+ """
217
+ query = {"term": {field: value}}
218
+ return self.search(query, size=size)
219
+
220
+ def search_by_match(self, field: str, value: str, size: int = 10) -> Optional[Dict[str, Any]]:
221
+ """Search documents by text match.
222
+
223
+ Args:
224
+ field: Field name to search
225
+ value: Text to match
226
+ size: Number of results to return
227
+
228
+ Returns:
229
+ Dict containing search results, or None if failed
230
+ """
231
+ query = {"match": {field: value}}
232
+ return self.search(query, size=size)
233
+
234
+ def search_by_match_phrase_prefix(self, field: str, value: str, size: int = 10) -> Optional[Dict[str, Any]]:
235
+ """Search documents by phrase prefix match.
236
+
237
+ Args:
238
+ field: Field name to search
239
+ value: Phrase prefix to match
240
+ size: Number of results to return
241
+
242
+ Returns:
243
+ Dict containing search results, or None if failed
244
+ """
245
+ query = {"match_phrase_prefix": {field: value}}
246
+ return self.search(query, size=size)
247
+
248
+ def search_all(self, size: int = 10) -> Optional[Dict[str, Any]]:
249
+ """Search all documents in the index.
250
+
251
+ Args:
252
+ size: Number of results to return
253
+
254
+ Returns:
255
+ Dict containing search results, or None if failed
256
+ """
257
+ query = {"match_all": {}}
258
+ return self.search(query, size=size)
259
+
260
+ # Health and Status Operations
261
+
262
+ def health_check(self) -> bool:
263
+ """Check if Elasticsearch is healthy.
264
+
265
+ Returns:
266
+ bool: True if healthy, False otherwise
267
+ """
268
+ try:
269
+ response = self._make_request("GET", f"{self.base_url}/_cluster/health")
270
+ return response.status_code == 200
271
+ except Exception:
272
+ return False
273
+
274
+ def get_cluster_info(self) -> Optional[Dict[str, Any]]:
275
+ """Get cluster information.
276
+
277
+ Returns:
278
+ Dict containing cluster info, or None if failed
279
+ """
280
+ try:
281
+ response = self._make_request("GET", f"{self.base_url}/_cluster/health")
282
+ if response.status_code == 200:
283
+ return response.json()
284
+ return None
285
+ except Exception:
286
+ return None
@@ -1,50 +1,92 @@
1
1
  import json
2
2
  import logging
3
3
  import asyncio
4
+ import os
4
5
  import threading
5
6
  from concurrent.futures import ThreadPoolExecutor
6
7
  from typing import Optional, Tuple, Any, Dict
7
8
  from datetime import datetime
8
- from urllib.parse import urlparse
9
- import requests
10
9
 
11
- from eval_protocol.types.remote_rollout_processor import ElasticSearchConfig
10
+ from eval_protocol.types.remote_rollout_processor import ElasticsearchConfig
11
+ from .elasticsearch_client import ElasticsearchClient
12
12
 
13
13
 
14
14
  class ElasticsearchDirectHttpHandler(logging.Handler):
15
- def __init__(self, elasticsearch_config: ElasticSearchConfig) -> None:
15
+ def __init__(self, elasticsearch_config: ElasticsearchConfig) -> None:
16
16
  super().__init__()
17
- self.base_url: str = elasticsearch_config.url.rstrip("/")
18
- self.index_name: str = elasticsearch_config.index_name
19
- self.api_key: str = elasticsearch_config.api_key
20
- self.url: str = f"{self.base_url}/{self.index_name}/_doc"
17
+ self.config = ElasticsearchConfig(
18
+ url=elasticsearch_config.url,
19
+ api_key=elasticsearch_config.api_key,
20
+ index_name=elasticsearch_config.index_name,
21
+ )
22
+ self.client = ElasticsearchClient(self.config)
21
23
  self.formatter: logging.Formatter = logging.Formatter()
22
24
  self._executor = None
23
25
 
24
- # Parse URL to determine if we should verify SSL
25
- parsed_url = urlparse(elasticsearch_config.url)
26
- self.verify_ssl = parsed_url.scheme == "https"
27
-
28
26
  def emit(self, record: logging.LogRecord) -> None:
29
27
  """Emit a log record by scheduling it for async transmission."""
30
28
  try:
31
29
  # Create proper ISO 8601 timestamp
32
30
  timestamp = datetime.fromtimestamp(record.created).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
33
31
 
32
+ rollout_id = self._get_rollout_id(record)
33
+ status_info = self._get_status_info(record)
34
+
34
35
  data: Dict[str, Any] = {
35
36
  "@timestamp": timestamp,
36
37
  "level": record.levelname,
37
38
  "message": record.getMessage(),
38
39
  "logger_name": record.name,
39
- # Add other relevant record attributes if needed
40
+ "rollout_id": rollout_id,
40
41
  }
41
42
 
43
+ # Add status information if present
44
+ if status_info:
45
+ data.update(status_info)
46
+
42
47
  # Schedule the HTTP request to run asynchronously
43
48
  self._schedule_async_send(data, record)
44
49
  except Exception as e:
45
50
  self.handleError(record)
46
51
  print(f"Error preparing log for Elasticsearch: {e}")
47
52
 
53
+ def _get_rollout_id(self, record: logging.LogRecord) -> str:
54
+ """Get the rollout ID from environment variables."""
55
+ rollout_id = os.getenv("EP_ROLLOUT_ID")
56
+ if rollout_id is None:
57
+ raise ValueError(
58
+ "EP_ROLLOUT_ID environment variable is not set but needed for ElasticsearchDirectHttpHandler"
59
+ )
60
+ return rollout_id
61
+
62
+ def _get_status_info(self, record: logging.LogRecord) -> Optional[Dict[str, Any]]:
63
+ """Extract status information from the log record's extra data."""
64
+ # Check if 'status' is in the extra data (passed via extra parameter)
65
+ if hasattr(record, "status") and record.status is not None: # type: ignore
66
+ status = record.status # type: ignore
67
+
68
+ # Handle Status class instances (Pydantic BaseModel)
69
+ if hasattr(status, "code") and hasattr(status, "message"):
70
+ # Status object - extract code and message
71
+ status_code = status.code
72
+ # Handle both enum values and direct integer values
73
+ if hasattr(status_code, "value"):
74
+ status_code = status_code.value
75
+
76
+ return {
77
+ "status_code": status_code,
78
+ "status_message": status.message,
79
+ "status_details": getattr(status, "details", []),
80
+ }
81
+ elif isinstance(status, dict):
82
+ # Dictionary representation of status
83
+ return {
84
+ "status_code": status.get("code"),
85
+ "status_message": status.get("message"),
86
+ "status_details": status.get("details", []),
87
+ }
88
+ return None
89
+
48
90
  def _schedule_async_send(self, data: Dict[str, Any], record: logging.LogRecord) -> None:
49
91
  """Schedule an async task to send the log data to Elasticsearch."""
50
92
  if self._executor is None:
@@ -59,13 +101,9 @@ class ElasticsearchDirectHttpHandler(logging.Handler):
59
101
  def _send_to_elasticsearch(self, data: Dict[str, Any], record: logging.LogRecord) -> None:
60
102
  """Send data to Elasticsearch (runs in thread pool)."""
61
103
  try:
62
- response: requests.Response = requests.post(
63
- self.url,
64
- headers={"Content-Type": "application/json", "Authorization": f"ApiKey {self.api_key}"},
65
- data=json.dumps(data),
66
- verify=self.verify_ssl, # If using HTTPS, verify SSL certificate
67
- )
68
- response.raise_for_status() # Raise an exception for HTTP errors
104
+ success = self.client.index_document(data)
105
+ if not success:
106
+ raise Exception("Failed to index document to Elasticsearch")
69
107
  except Exception as e:
70
108
  # Re-raise to be handled by the callback
71
109
  raise e
@@ -1,6 +1,6 @@
1
- import requests
2
1
  from typing import Dict, Any, Optional
3
- from urllib.parse import urlparse
2
+ from .elasticsearch_client import ElasticsearchClient
3
+ from eval_protocol.types.remote_rollout_processor import ElasticsearchConfig
4
4
 
5
5
 
6
6
  class ElasticsearchIndexManager:
@@ -14,16 +14,10 @@ class ElasticsearchIndexManager:
14
14
  index_name: Name of the index to manage
15
15
  api_key: API key for authentication
16
16
  """
17
- self.base_url: str = base_url.rstrip("/")
18
- self.index_name: str = index_name
19
- self.api_key: str = api_key
20
- self.index_url: str = f"{self.base_url}/{self.index_name}"
17
+ self.config = ElasticsearchConfig(url=base_url, api_key=api_key, index_name=index_name)
18
+ self.client = ElasticsearchClient(self.config)
21
19
  self._mapping_created: bool = False
22
20
 
23
- # Parse URL to determine if we should verify SSL
24
- parsed_url = urlparse(base_url)
25
- self.verify_ssl = parsed_url.scheme == "https"
26
-
27
21
  def create_logging_index_mapping(self) -> bool:
28
22
  """Create index with proper mapping for logging data.
29
23
 
@@ -41,25 +35,22 @@ class ElasticsearchIndexManager:
41
35
 
42
36
  # If index exists but has wrong mapping, delete and recreate it
43
37
  if self.index_exists():
44
- print(f"Warning: Index {self.index_name} exists with incorrect mapping. Deleting and recreating...")
38
+ print(
39
+ f"Warning: Index {self.config.index_name} exists with incorrect mapping. Deleting and recreating..."
40
+ )
45
41
  if not self.delete_index():
46
- print(f"Warning: Failed to delete existing index {self.index_name}")
42
+ print(f"Warning: Failed to delete existing index {self.config.index_name}")
47
43
  return False
48
44
 
49
45
  # Create index with proper mapping
50
46
  mapping = self._get_logging_mapping()
51
- response = requests.put(
52
- self.index_url,
53
- headers={"Content-Type": "application/json", "Authorization": f"ApiKey {self.api_key}"},
54
- json=mapping,
55
- verify=self.verify_ssl,
56
- )
57
-
58
- if response.status_code in [200, 201]:
47
+ success = self.client.create_index(mapping)
48
+
49
+ if success:
59
50
  self._mapping_created = True
60
51
  return True
61
52
  else:
62
- print(f"Warning: Failed to create index mapping: {response.status_code} - {response.text}")
53
+ print("Warning: Failed to create index mapping")
63
54
  return False
64
55
 
65
56
  except Exception as e:
@@ -74,46 +65,50 @@ class ElasticsearchIndexManager:
74
65
  """
75
66
  try:
76
67
  # Check if index exists
77
- response = requests.head(
78
- self.index_url, headers={"Authorization": f"ApiKey {self.api_key}"}, verify=self.verify_ssl
79
- )
80
-
81
- if response.status_code != 200:
68
+ if not self.client.index_exists():
82
69
  return False
83
70
 
84
71
  # Check if mapping is correct
85
- mapping_response = requests.get(
86
- f"{self.index_url}/_mapping",
87
- headers={"Authorization": f"ApiKey {self.api_key}"},
88
- verify=self.verify_ssl,
89
- )
90
-
91
- if mapping_response.status_code != 200:
72
+ mapping_data = self.client.get_mapping()
73
+ if mapping_data is None:
92
74
  return False
93
75
 
94
- mapping_data = mapping_response.json()
95
76
  return self._has_correct_timestamp_mapping(mapping_data)
96
77
 
97
78
  except Exception:
98
79
  return False
99
80
 
100
81
  def _has_correct_timestamp_mapping(self, mapping_data: Dict[str, Any]) -> bool:
101
- """Check if the mapping has @timestamp as a date field.
82
+ """Check if the mapping has @timestamp as a date field, rollout_id as a keyword field, and status fields.
102
83
 
103
84
  Args:
104
85
  mapping_data: Elasticsearch mapping response data
105
86
 
106
87
  Returns:
107
- bool: True if @timestamp is correctly mapped as date field
88
+ bool: True if all required fields are correctly mapped
108
89
  """
109
90
  try:
110
- return (
111
- self.index_name in mapping_data
112
- and "mappings" in mapping_data[self.index_name]
113
- and "properties" in mapping_data[self.index_name]["mappings"]
114
- and "@timestamp" in mapping_data[self.index_name]["mappings"]["properties"]
115
- and mapping_data[self.index_name]["mappings"]["properties"]["@timestamp"].get("type") == "date"
116
- )
91
+ if not (
92
+ self.config.index_name in mapping_data
93
+ and "mappings" in mapping_data[self.config.index_name]
94
+ and "properties" in mapping_data[self.config.index_name]["mappings"]
95
+ ):
96
+ return False
97
+
98
+ properties = mapping_data[self.config.index_name]["mappings"]["properties"]
99
+
100
+ # Check @timestamp is mapped as date
101
+ timestamp_ok = "@timestamp" in properties and properties["@timestamp"].get("type") == "date"
102
+
103
+ # Check rollout_id is mapped as keyword
104
+ rollout_id_ok = "rollout_id" in properties and properties["rollout_id"].get("type") == "keyword"
105
+
106
+ # Check status fields are mapped correctly
107
+ status_code_ok = "status_code" in properties and properties["status_code"].get("type") == "integer"
108
+ status_message_ok = "status_message" in properties and properties["status_message"].get("type") == "text"
109
+ status_details_ok = "status_details" in properties and properties["status_details"].get("type") == "object"
110
+
111
+ return timestamp_ok and rollout_id_ok and status_code_ok and status_message_ok and status_details_ok
117
112
  except (KeyError, TypeError):
118
113
  return False
119
114
 
@@ -130,6 +125,10 @@ class ElasticsearchIndexManager:
130
125
  "level": {"type": "keyword"},
131
126
  "message": {"type": "text"},
132
127
  "logger_name": {"type": "keyword"},
128
+ "rollout_id": {"type": "keyword"},
129
+ "status_code": {"type": "integer"},
130
+ "status_message": {"type": "text"},
131
+ "status_details": {"type": "object"},
133
132
  }
134
133
  }
135
134
  }
@@ -141,14 +140,12 @@ class ElasticsearchIndexManager:
141
140
  bool: True if index was deleted successfully, False otherwise.
142
141
  """
143
142
  try:
144
- response = requests.delete(
145
- self.index_url, headers={"Authorization": f"ApiKey {self.api_key}"}, verify=self.verify_ssl
146
- )
147
- if response.status_code in [200, 404]: # 404 means index doesn't exist, which is fine
143
+ success = self.client.delete_index()
144
+ if success:
148
145
  self._mapping_created = False
149
146
  return True
150
147
  else:
151
- print(f"Warning: Failed to delete index: {response.status_code} - {response.text}")
148
+ print("Warning: Failed to delete index")
152
149
  return False
153
150
  except Exception as e:
154
151
  print(f"Warning: Failed to delete index: {e}")
@@ -160,13 +157,7 @@ class ElasticsearchIndexManager:
160
157
  Returns:
161
158
  bool: True if index exists, False otherwise.
162
159
  """
163
- try:
164
- response = requests.head(
165
- self.index_url, headers={"Authorization": f"ApiKey {self.api_key}"}, verify=self.verify_ssl
166
- )
167
- return response.status_code == 200
168
- except Exception:
169
- return False
160
+ return self.client.index_exists()
170
161
 
171
162
  def get_index_stats(self) -> Optional[Dict[str, Any]]:
172
163
  """Get statistics about the index.
@@ -174,14 +165,4 @@ class ElasticsearchIndexManager:
174
165
  Returns:
175
166
  Dict containing index statistics, or None if failed
176
167
  """
177
- try:
178
- response = requests.get(
179
- f"{self.index_url}/_stats",
180
- headers={"Authorization": f"ApiKey {self.api_key}"},
181
- verify=self.verify_ssl,
182
- )
183
- if response.status_code == 200:
184
- return response.json()
185
- return None
186
- except Exception:
187
- return None
168
+ return self.client.get_index_stats()
@@ -6,7 +6,7 @@ from typing import Optional
6
6
 
7
7
  from dotenv import load_dotenv
8
8
  from eval_protocol.directory_utils import find_eval_protocol_dir
9
- from eval_protocol.types.remote_rollout_processor import ElasticSearchConfig
9
+ from eval_protocol.types.remote_rollout_processor import ElasticsearchConfig
10
10
  from eval_protocol.logging.elasticsearch_index_manager import ElasticsearchIndexManager
11
11
 
12
12
  logger = logging.getLogger(__name__)
@@ -24,7 +24,7 @@ class ElasticsearchSetup:
24
24
  def __init__(self):
25
25
  self.eval_protocol_dir = find_eval_protocol_dir()
26
26
 
27
- def setup_elasticsearch(self, index_name: str = "default-logs") -> ElasticSearchConfig:
27
+ def setup_elasticsearch(self, index_name: str = "default-logs") -> ElasticsearchConfig:
28
28
  """
29
29
  Set up Elasticsearch, handling both local and remote scenarios.
30
30
 
@@ -32,7 +32,7 @@ class ElasticsearchSetup:
32
32
  index_name: Name of the Elasticsearch index to use for logging
33
33
 
34
34
  Returns:
35
- ElasticSearchConfig for the running instance with the specified index name.
35
+ ElasticsearchConfig for the running instance with the specified index name.
36
36
  """
37
37
  elastic_start_local_dir = os.path.join(self.eval_protocol_dir, "elastic-start-local")
38
38
  env_file_path = os.path.join(elastic_start_local_dir, ".env")
@@ -48,11 +48,11 @@ class ElasticsearchSetup:
48
48
  self.create_logging_index(index_name)
49
49
 
50
50
  # Return config with the specified index name
51
- return ElasticSearchConfig(url=config.url, api_key=config.api_key, index_name=index_name)
51
+ return ElasticsearchConfig(url=config.url, api_key=config.api_key, index_name=index_name)
52
52
 
53
53
  def _setup_existing_docker_elasticsearch(
54
54
  self, elastic_start_local_dir: str, env_file_path: str
55
- ) -> ElasticSearchConfig:
55
+ ) -> ElasticsearchConfig:
56
56
  """Set up Elasticsearch using existing Docker start.sh script."""
57
57
  from eval_protocol.utils.subprocess_utils import run_script_and_wait
58
58
 
@@ -63,7 +63,7 @@ class ElasticsearchSetup:
63
63
  )
64
64
  return self._parse_elastic_env_file(env_file_path)
65
65
 
66
- def _setup_initialized_docker_elasticsearch(self, env_file_path: str) -> ElasticSearchConfig:
66
+ def _setup_initialized_docker_elasticsearch(self, env_file_path: str) -> ElasticsearchConfig:
67
67
  """Set up Elasticsearch by initializing Docker setup from scratch with retry logic."""
68
68
  max_retries = 2
69
69
  for attempt in range(max_retries):
@@ -126,7 +126,7 @@ class ElasticsearchSetup:
126
126
  return False
127
127
  return False
128
128
 
129
- def _parse_elastic_env_file(self, env_file_path: str) -> ElasticSearchConfig:
129
+ def _parse_elastic_env_file(self, env_file_path: str) -> ElasticsearchConfig:
130
130
  """Parse ES_LOCAL_API_KEY and ES_LOCAL_URL from .env file."""
131
131
  loaded = load_dotenv(env_file_path)
132
132
  if not loaded:
@@ -138,7 +138,7 @@ class ElasticsearchSetup:
138
138
  if not url or not api_key:
139
139
  raise ElasticsearchSetupError("Failed to parse ES_LOCAL_API_KEY and ES_LOCAL_URL from .env file")
140
140
 
141
- return ElasticSearchConfig(url=url, api_key=api_key, index_name="default-logs")
141
+ return ElasticsearchConfig(url=url, api_key=api_key, index_name="default-logs")
142
142
 
143
143
  def create_logging_index(self, index_name: str) -> bool:
144
144
  """Create an Elasticsearch index with proper mapping for logging data.
@@ -4,9 +4,10 @@ from typing import Any, Dict, List, Optional, Callable
4
4
 
5
5
  import requests
6
6
 
7
+ from eval_protocol.logging.elasticsearch_client import ElasticsearchClient
7
8
  from eval_protocol.models import EvaluationRow, Status
8
9
  from eval_protocol.data_loader.dynamic_data_loader import DynamicDataLoader
9
- from eval_protocol.types.remote_rollout_processor import ElasticSearchConfig, InitRequest, RolloutMetadata
10
+ from eval_protocol.types.remote_rollout_processor import ElasticsearchConfig, InitRequest, RolloutMetadata
10
11
  from .rollout_processor import RolloutProcessor
11
12
  from .types import RolloutProcessorConfig
12
13
  from .elasticsearch_setup import ElasticsearchSetup
@@ -33,7 +34,7 @@ class RemoteRolloutProcessor(RolloutProcessor):
33
34
  timeout_seconds: float = 120.0,
34
35
  output_data_loader: Callable[[str], DynamicDataLoader],
35
36
  disable_elastic_search: bool = False,
36
- elastic_search_config: Optional[ElasticSearchConfig] = None,
37
+ elastic_search_config: Optional[ElasticsearchConfig] = None,
37
38
  ):
38
39
  # Prefer constructor-provided configuration. These can be overridden via
39
40
  # config.kwargs at call time for backward compatibility.
@@ -56,7 +57,7 @@ class RemoteRolloutProcessor(RolloutProcessor):
56
57
  self._elastic_search_config = self._setup_elastic_search()
57
58
  logger.info("Elasticsearch setup complete")
58
59
 
59
- def _setup_elastic_search(self) -> ElasticSearchConfig:
60
+ def _setup_elastic_search(self) -> ElasticsearchConfig:
60
61
  """Set up Elasticsearch using the dedicated setup module."""
61
62
  setup = ElasticsearchSetup()
62
63
  return setup.setup_elasticsearch()
@@ -183,6 +184,10 @@ class RemoteRolloutProcessor(RolloutProcessor):
183
184
  r.raise_for_status()
184
185
  return r.json()
185
186
 
187
+ elasticsearch_client = (
188
+ ElasticsearchClient(self._elastic_search_config) if self._elastic_search_config else None
189
+ )
190
+
186
191
  while time.time() < deadline:
187
192
  try:
188
193
  status = await asyncio.to_thread(_get_status)
@@ -4,10 +4,11 @@ Request and response models for remote rollout processor servers.
4
4
 
5
5
  from typing import Any, Dict, List, Optional
6
6
  from pydantic import BaseModel, Field
7
+ from urllib.parse import urlparse
7
8
  from eval_protocol.models import Message, Status
8
9
 
9
10
 
10
- class ElasticSearchConfig(BaseModel):
11
+ class ElasticsearchConfig(BaseModel):
11
12
  """
12
13
  Configuration for Elasticsearch.
13
14
  """
@@ -16,6 +17,12 @@ class ElasticSearchConfig(BaseModel):
16
17
  api_key: str
17
18
  index_name: str
18
19
 
20
+ @property
21
+ def verify_ssl(self) -> bool:
22
+ """Infer verify_ssl from URL scheme."""
23
+ parsed_url = urlparse(self.url)
24
+ return parsed_url.scheme == "https"
25
+
19
26
 
20
27
  class RolloutMetadata(BaseModel):
21
28
  """Metadata for rollout execution."""
@@ -31,7 +38,7 @@ class InitRequest(BaseModel):
31
38
  """Request model for POST /init endpoint."""
32
39
 
33
40
  model: str
34
- elastic_search_config: Optional[ElasticSearchConfig] = None
41
+ elastic_search_config: Optional[ElasticsearchConfig] = None
35
42
  messages: Optional[List[Message]] = None
36
43
  tools: Optional[List[Dict[str, Any]]] = None
37
44
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eval-protocol
3
- Version: 0.2.35.dev1
3
+ Version: 0.2.35.dev2
4
4
  Summary: The official Python SDK for Eval Protocol (EP.) EP is an open protocol that standardizes how developers author evals for large language model (LLM) applications.
5
5
  Author-email: Fireworks AI <info@fireworks.ai>
6
6
  License-Expression: MIT
@@ -3,11 +3,11 @@ development/normalize_sandbox_fusion.py,sha256=1598fX-W6MNM2QZusUEurOLvlMv0-a8-T
3
3
  development/utils/__init__.py,sha256=5_3ae0whpBdWIYreHlWCc2H9BCCsQfuVFSKZbxXID60,79
4
4
  development/utils/generate_api_key.py,sha256=hHCMFkzW4yxqwcn2ct5diDm-PR9cMX9XP7IYieZvH5U,869
5
5
  development/utils/subprocess_manager.py,sha256=7n7rT9ji7h93i79SMrGS5RNesrnLFjdFON9_eQCmYNE,18937
6
- eval_protocol/__init__.py,sha256=KSI4bHfk9SoXWXqMBWDyjvz5RW2kFT6zt1pAC70ZEgA,3321
6
+ eval_protocol/__init__.py,sha256=n0wwL4ueoazupJc8Enjqt8C917hHT4FcrsjqjZ_e9XU,3343
7
7
  eval_protocol/__main__.py,sha256=FIW5fo2X2rsPThurSlZEuqvE1u0XNwJW1uoej_OhAAs,161
8
- eval_protocol/_version.py,sha256=m3jeKhOXL0PZa1Bvksj29LbXTi8qC6PJXuT42NtlB8w,503
8
+ eval_protocol/_version.py,sha256=BtvZ7LNFVsPmnGr9QV8lMcCn8Yl3uTtapUP8aIqg6Us,503
9
9
  eval_protocol/auth.py,sha256=6KOY-IIfrYkWmvZj3N0qrVHHCGbOFmcr2F73ZxIoYTg,5854
10
- eval_protocol/cli.py,sha256=A1acUJz6IVSFcVU5CrGSUXSOax0wj4LP4VOF5hCzga4,18905
10
+ eval_protocol/cli.py,sha256=otIJKB_TjmuO9KPCBvfHqjkmySZW2XTMZgqXIoVSzAE,18768
11
11
  eval_protocol/common_utils.py,sha256=eRzO_SQNfxBz-vIF9PdTVFAzuAb2zfuCnKSKY_xdKQY,2491
12
12
  eval_protocol/config.py,sha256=b6mWbw-IJhu8iWgCJqcFp14vPJl7iXH7FnKe7cNIgSc,6438
13
13
  eval_protocol/directory_utils.py,sha256=6sYCgXt84qrQk1fGNtOZKj_Z7z9XK1e5jyMDsbmaiAQ,1869
@@ -63,13 +63,13 @@ eval_protocol/benchmarks/test_tau_bench_retail.py,sha256=-rGNA_biYO4_3d9S2JMHTQ6
63
63
  eval_protocol/benchmarks/data/airline_dataset.jsonl,sha256=jVvKx51hy4eUSia5zXZRI_N5_X6RRHi192_YaaBH7n4,79086
64
64
  eval_protocol/benchmarks/data/retail_dataset.jsonl,sha256=9fGs1U6lvnpKBNjERWPmJHOnrO3cPoDtGwvdxv11mD4,179853
65
65
  eval_protocol/cli_commands/__init__.py,sha256=XYAFq-qJ787pmrMiouG_jd9uwZcRhxLtNLSTxKcHzCM,85
66
- eval_protocol/cli_commands/agent_eval_cmd.py,sha256=7ouxAWU-1OzcR3O_MuCEvCy1oLy8dxNPuNmKrl7YqRs,11201
66
+ eval_protocol/cli_commands/agent_eval_cmd.py,sha256=ezENJzLsUHvpm-FXrDpIV4fBHZbr3WqQOQiIL_rcVEw,11032
67
67
  eval_protocol/cli_commands/common.py,sha256=UuV5AZ02Rp6oioOblr9mfIrFcfTAKJOuxzhOqadBcHE,10155
68
68
  eval_protocol/cli_commands/deploy.py,sha256=WwwyK80exi1dBFjcIXEj_odaGOfe1byiWgnLGiUUQvM,24054
69
69
  eval_protocol/cli_commands/deploy_mcp.py,sha256=_6S7NcbCb_hGVqaBUgKLURO53i2UAI7xXUkBVXP6cLo,9961
70
70
  eval_protocol/cli_commands/logs.py,sha256=gh-HIQ51SH8nyvmdpHBfzFSJg7TnL0QwvVN9vpDa-20,799
71
71
  eval_protocol/cli_commands/preview.py,sha256=1iXru9V8QBGx7tLDJ28iG7bjV2ICFroEKaQ8EJsGciU,8072
72
- eval_protocol/cli_commands/run_eval_cmd.py,sha256=rJn-pGlPbWcXljywH78kif7r_m_CW84LjGa7xyKgNvo,9725
72
+ eval_protocol/cli_commands/run_eval_cmd.py,sha256=xLf7Adfjt-5jmfKHh9ioay3n0MDo78tofeZgTnZ-C7w,9730
73
73
  eval_protocol/cli_commands/upload.py,sha256=al9CU4syDf5m0KOtlIol3tDb8WSMibNZV4-fqYqBEUU,19776
74
74
  eval_protocol/data_loader/__init__.py,sha256=tUntIfAZ0D2yjwfYMLzkDvdDsWqsnYS73JtngubrjFo,153
75
75
  eval_protocol/data_loader/dynamic_data_loader.py,sha256=tMSqKiECls1ySHv5eHpAlc9PwsRLzE5Rp4Ko3kIYUuA,1442
@@ -99,8 +99,9 @@ eval_protocol/integrations/__init__.py,sha256=mTs7Wypur0uG9r0GSE-MtdIx0eXZjecplm
99
99
  eval_protocol/integrations/deepeval.py,sha256=BZsm_EYKFIIoJdLjp29tcpKnSWpGB0x1uuf3LxsoBjc,4837
100
100
  eval_protocol/integrations/openeval.py,sha256=bHn0PRvtHMWmrKbGHqZOvJfx7lmvjt4vzeGdx6oqmck,1443
101
101
  eval_protocol/integrations/trl.py,sha256=yaF7GAq9xRhIhNvr0m0lul-_WZ8H6uiY_e3qDCUQYMY,9404
102
- eval_protocol/logging/elasticsearch_direct_http_handler.py,sha256=x6Y_BUE9KHsA7btS0EdPmTaJPIAZnHmuZ68p7tslHw0,3962
103
- eval_protocol/logging/elasticsearch_index_manager.py,sha256=ddeXO8zyXIkw27m9aVKLb1UJzUdxk39zGIU9BXAGeTA,6806
102
+ eval_protocol/logging/elasticsearch_client.py,sha256=OhecOYRF2fkneDFGRnvTHgsXtbP3Z3rfd3BXeZzauNA,8857
103
+ eval_protocol/logging/elasticsearch_direct_http_handler.py,sha256=ZYfgKod54pLMw3f__u6m8tHdSgojNS-4JmWiSLz-ewk,5503
104
+ eval_protocol/logging/elasticsearch_index_manager.py,sha256=nZvieLATW_QLy1d6DL824JYY7V04IxSal--IGYCS1R4,6339
104
105
  eval_protocol/mcp/__init__.py,sha256=H2lau7Ch0-CBTy0zFBeyKJ-a1lHxia6WlXWLlu-rflM,1353
105
106
  eval_protocol/mcp/adapter.py,sha256=aXETSXkOKFg8QdYcfcpJoDMqdwgmaEKrjCkHui-aO3o,4020
106
107
  eval_protocol/mcp/clients.py,sha256=K00KrQdaSnHy0SgSQzjAmJjhHVnnaKT7tqtgAeV13lY,8982
@@ -146,7 +147,7 @@ eval_protocol/pytest/default_no_op_rollout_processor.py,sha256=d6Uh-lRQVtLHbb2BC
146
147
  eval_protocol/pytest/default_pydantic_ai_rollout_processor.py,sha256=L_gFvDNTKr9cFV8ngpg-iIrEycB79nUVowRvPkxysm8,7746
147
148
  eval_protocol/pytest/default_single_turn_rollout_process.py,sha256=hG8a605vOuzd-ITcpFOU2ysU2v8-Bshs-LyLeM4Tfig,5906
148
149
  eval_protocol/pytest/dual_mode_wrapper.py,sha256=wMRD0G4-GpFvh2ZaBxhcrRdNSxCWIi0RqhZ5eBJVMio,3969
149
- eval_protocol/pytest/elasticsearch_setup.py,sha256=3GJmohHfsLsP9gAVfioqwI7C6aONEWAK1XSkVTdEUxQ,7057
150
+ eval_protocol/pytest/elasticsearch_setup.py,sha256=6NEljVFyeyrJXfS0TUmq1kEoJGr5vJZ4_Kw00UmrJes,7057
150
151
  eval_protocol/pytest/evaluation_test.py,sha256=tQ_lPrzb14tYOuhIPhHJYDRTk_4PYdzFHb7CIJ9j628,34729
151
152
  eval_protocol/pytest/evaluation_test_postprocess.py,sha256=OyLTqvHDgvV4cbzyJnH0AC_L_k3vepe8XQus0ZTtgLE,9449
152
153
  eval_protocol/pytest/exception_config.py,sha256=VvoVmUYkWZi_ZtpdFyTtJlHjpdtr-MbvW5fKZzLpRUQ,4219
@@ -155,7 +156,7 @@ eval_protocol/pytest/generate_parameter_combinations.py,sha256=LTVn6zxR4pPdKB-fp
155
156
  eval_protocol/pytest/handle_persist_flow.py,sha256=YBqXEP8gZ6noJK1AGOONpRRZMB3dQNEZLpM1kEHt7CE,10925
156
157
  eval_protocol/pytest/parameterize.py,sha256=Ry74Kr_ciSLcVg1AeKIKUNqnlkjZZ4xuIv5kmd_mm3s,16091
157
158
  eval_protocol/pytest/plugin.py,sha256=TxnWls2plEbm5EJx3gJRBNqkDMv-paWdjg0wyM9-fb8,14371
158
- eval_protocol/pytest/remote_rollout_processor.py,sha256=wDkdFthkbEW-LruG6rKXVPYi2M_YP3f7ZaMBSbAhWh8,11744
159
+ eval_protocol/pytest/remote_rollout_processor.py,sha256=NjYJUpWT_1Wk8h_W5LHQfcMS6UKm9Uj9FraO4qgB8Ew,11977
159
160
  eval_protocol/pytest/rollout_processor.py,sha256=eKm44_sNU3yIVcfk7xsIUTXvAnCzjd0CNSw6PuYQAGw,791
160
161
  eval_protocol/pytest/store_experiment_link.py,sha256=Sb7DTXrDUcPUp1LBE746R7vulyEtDszPPTLa21AzFgs,1447
161
162
  eval_protocol/pytest/store_results_url.py,sha256=SmAPVDD--u_hZ6GuB_L3GVWUvKxkczF1etjRTnztqu0,1726
@@ -196,7 +197,7 @@ eval_protocol/stats/__init__.py,sha256=FtH_CJCY4SLhJeTJmBrH4ISYJX8PpRYpL4R8NQSeU
196
197
  eval_protocol/stats/confidence_intervals.py,sha256=zreh2-8dTq2OsiShMBZezezu6NobskXqXrznMS6srm0,4007
197
198
  eval_protocol/types/__init__.py,sha256=UisnRwa-7haDyLZg6U6qiVNWh3eJ-xmsPk5nNycOtkY,243
198
199
  eval_protocol/types/errors.py,sha256=4SZFJbXaruWg6nsTLg2HezEP5MNA9zJSrCxLnaxUfN4,514
199
- eval_protocol/types/remote_rollout_processor.py,sha256=BYCaMz-rRWwqLzuvLQBxd6tqdpeFtaMt9T8XgrsMOBA,1801
200
+ eval_protocol/types/remote_rollout_processor.py,sha256=2S9PgcZC7lTWym9EvIzIWlSZOfrU11llkuI_jlWwS1U,2016
200
201
  eval_protocol/types/types.py,sha256=FPpnMu8Q1DTyq3EcG1WtpkHaDMPuodG1zNpqnExIPbw,3523
201
202
  eval_protocol/utils/__init__.py,sha256=bP-yexMWhOz40d2-vI8ZvA84uZOL3o0XWilc_H5dT8g,429
202
203
  eval_protocol/utils/batch_evaluation.py,sha256=tQfDmvsgTPT8H9t3j-cIpmkHXP2ib2iyLbvI8Yx1xKY,9260
@@ -210,7 +211,7 @@ eval_protocol/utils/show_results_url.py,sha256=PHM6dWtCUiuV5WQgvHegnxY7ofkE4b9wO
210
211
  eval_protocol/utils/static_policy.py,sha256=fiKnOS06EG5OB6p5An_yY_dLAvVboYnC4Sqx5z_v3-g,10716
211
212
  eval_protocol/utils/subprocess_utils.py,sha256=2EcoVNLSlfdxwQn-2pscqjiGpBR4Ho8kfRnmzmew-1w,3504
212
213
  eval_protocol/utils/vite_server.py,sha256=oKUEfOYbNQoq90i7nrk1xiDWpfK_9BS5eMNe0QEBSi4,5064
213
- eval_protocol-0.2.35.dev1.dist-info/licenses/LICENSE,sha256=OzeIb507xW9AVhGMqqHpoL_EFRJUo8Sb7A3LN5NqFfQ,1075
214
+ eval_protocol-0.2.35.dev2.dist-info/licenses/LICENSE,sha256=OzeIb507xW9AVhGMqqHpoL_EFRJUo8Sb7A3LN5NqFfQ,1075
214
215
  vendor/tau2/__init__.py,sha256=EQMX_v8x-YBV24ia35_nLkf5MrC6aAuT_M5m7IJcl3k,541
215
216
  vendor/tau2/cli.py,sha256=lhJocXCDxEfdv7gIxya5b0w5J5qebpgrg_ZTpjGp_ww,7515
216
217
  vendor/tau2/config.py,sha256=LrkKRGSFH4Cvf9CNO-MttJMvIia0a2zP1uKVnUQi6B8,1278
@@ -310,8 +311,8 @@ vite-app/dist/assets/index-C8woq7EO.js,sha256=UQiM5fwp8CGPSEnS5uAUQBzyOhfpTiMz8L
310
311
  vite-app/dist/assets/index-C8woq7EO.js.map,sha256=s3rEcnsitUjfuvHIg7nl4UHHG4wNIySAT3s-2YGDBFY,3849814
311
312
  vite-app/dist/assets/index-CSKGq1w7.css,sha256=o0IZC5dbIyMb7S9-gw8t5eK60RCXf3jsEPx9q2CYOAQ,24505
312
313
  vite-app/dist/assets/logo-light-BprIBJQW.png,sha256=rRXC24eqrQO3y--N493THrD48WQVAhSVMHM_iDKy250,21694
313
- eval_protocol-0.2.35.dev1.dist-info/METADATA,sha256=OSa-5gSfprsnvVRX8dDw_9MhO7fRqRrkiMkEabJ4Fuo,7450
314
- eval_protocol-0.2.35.dev1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
315
- eval_protocol-0.2.35.dev1.dist-info/entry_points.txt,sha256=CebRaxbWXly21zPN1fbyAw26kNUU2dv7zZyGkXxtFVw,183
316
- eval_protocol-0.2.35.dev1.dist-info/top_level.txt,sha256=8jjn7dpvLPL4RX2JBeAfPPMOR6x6f7E4o4yFiKLEHuw,33
317
- eval_protocol-0.2.35.dev1.dist-info/RECORD,,
314
+ eval_protocol-0.2.35.dev2.dist-info/METADATA,sha256=U1rm2RbgpeOYYvJ8CuhE6GrvWe0CyjBe2DN9iL2bAC0,7450
315
+ eval_protocol-0.2.35.dev2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
316
+ eval_protocol-0.2.35.dev2.dist-info/entry_points.txt,sha256=CebRaxbWXly21zPN1fbyAw26kNUU2dv7zZyGkXxtFVw,183
317
+ eval_protocol-0.2.35.dev2.dist-info/top_level.txt,sha256=8jjn7dpvLPL4RX2JBeAfPPMOR6x6f7E4o4yFiKLEHuw,33
318
+ eval_protocol-0.2.35.dev2.dist-info/RECORD,,