hackagent 0.1.0__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.
- hackagent/__init__.py +23 -0
- hackagent/agent.py +193 -0
- hackagent/api/__init__.py +1 -0
- hackagent/api/agent/__init__.py +1 -0
- hackagent/api/agent/agent_create.py +340 -0
- hackagent/api/agent/agent_destroy.py +136 -0
- hackagent/api/agent/agent_list.py +234 -0
- hackagent/api/agent/agent_partial_update.py +354 -0
- hackagent/api/agent/agent_retrieve.py +227 -0
- hackagent/api/agent/agent_update.py +354 -0
- hackagent/api/attack/__init__.py +1 -0
- hackagent/api/attack/attack_create.py +264 -0
- hackagent/api/attack/attack_destroy.py +140 -0
- hackagent/api/attack/attack_list.py +242 -0
- hackagent/api/attack/attack_partial_update.py +278 -0
- hackagent/api/attack/attack_retrieve.py +235 -0
- hackagent/api/attack/attack_update.py +278 -0
- hackagent/api/key/__init__.py +1 -0
- hackagent/api/key/key_create.py +168 -0
- hackagent/api/key/key_destroy.py +97 -0
- hackagent/api/key/key_list.py +158 -0
- hackagent/api/key/key_retrieve.py +150 -0
- hackagent/api/prompt/__init__.py +1 -0
- hackagent/api/prompt/prompt_create.py +160 -0
- hackagent/api/prompt/prompt_destroy.py +98 -0
- hackagent/api/prompt/prompt_list.py +173 -0
- hackagent/api/prompt/prompt_partial_update.py +174 -0
- hackagent/api/prompt/prompt_retrieve.py +151 -0
- hackagent/api/prompt/prompt_update.py +174 -0
- hackagent/api/result/__init__.py +1 -0
- hackagent/api/result/result_create.py +160 -0
- hackagent/api/result/result_destroy.py +98 -0
- hackagent/api/result/result_list.py +233 -0
- hackagent/api/result/result_partial_update.py +178 -0
- hackagent/api/result/result_retrieve.py +151 -0
- hackagent/api/result/result_trace_create.py +178 -0
- hackagent/api/result/result_update.py +174 -0
- hackagent/api/run/__init__.py +1 -0
- hackagent/api/run/run_create.py +172 -0
- hackagent/api/run/run_destroy.py +104 -0
- hackagent/api/run/run_list.py +260 -0
- hackagent/api/run/run_partial_update.py +186 -0
- hackagent/api/run/run_result_create.py +178 -0
- hackagent/api/run/run_retrieve.py +163 -0
- hackagent/api/run/run_run_tests_create.py +172 -0
- hackagent/api/run/run_update.py +186 -0
- hackagent/attacks/AdvPrefix/README.md +7 -0
- hackagent/attacks/AdvPrefix/__init__.py +0 -0
- hackagent/attacks/AdvPrefix/completer.py +438 -0
- hackagent/attacks/AdvPrefix/config.py +59 -0
- hackagent/attacks/AdvPrefix/preprocessing.py +521 -0
- hackagent/attacks/AdvPrefix/scorer.py +259 -0
- hackagent/attacks/AdvPrefix/scorer_parser.py +498 -0
- hackagent/attacks/AdvPrefix/selector.py +246 -0
- hackagent/attacks/AdvPrefix/step1_generate.py +324 -0
- hackagent/attacks/AdvPrefix/step4_compute_ce.py +293 -0
- hackagent/attacks/AdvPrefix/step6_get_completions.py +387 -0
- hackagent/attacks/AdvPrefix/step7_evaluate_responses.py +289 -0
- hackagent/attacks/AdvPrefix/step8_aggregate_evaluations.py +177 -0
- hackagent/attacks/AdvPrefix/step9_select_prefixes.py +59 -0
- hackagent/attacks/AdvPrefix/utils.py +192 -0
- hackagent/attacks/__init__.py +6 -0
- hackagent/attacks/advprefix.py +1136 -0
- hackagent/attacks/base.py +50 -0
- hackagent/attacks/strategies.py +539 -0
- hackagent/branding.py +143 -0
- hackagent/client.py +328 -0
- hackagent/errors.py +31 -0
- hackagent/logger.py +67 -0
- hackagent/models/__init__.py +71 -0
- hackagent/models/agent.py +240 -0
- hackagent/models/agent_request.py +169 -0
- hackagent/models/agent_type_enum.py +12 -0
- hackagent/models/attack.py +154 -0
- hackagent/models/attack_request.py +82 -0
- hackagent/models/evaluation_status_enum.py +14 -0
- hackagent/models/organization_minimal.py +68 -0
- hackagent/models/paginated_agent_list.py +123 -0
- hackagent/models/paginated_attack_list.py +123 -0
- hackagent/models/paginated_prompt_list.py +123 -0
- hackagent/models/paginated_result_list.py +123 -0
- hackagent/models/paginated_run_list.py +123 -0
- hackagent/models/paginated_user_api_key_list.py +123 -0
- hackagent/models/patched_agent_request.py +176 -0
- hackagent/models/patched_attack_request.py +92 -0
- hackagent/models/patched_prompt_request.py +162 -0
- hackagent/models/patched_result_request.py +237 -0
- hackagent/models/patched_run_request.py +138 -0
- hackagent/models/prompt.py +226 -0
- hackagent/models/prompt_request.py +155 -0
- hackagent/models/result.py +294 -0
- hackagent/models/result_list_evaluation_status.py +14 -0
- hackagent/models/result_request.py +232 -0
- hackagent/models/run.py +233 -0
- hackagent/models/run_list_status.py +12 -0
- hackagent/models/run_request.py +133 -0
- hackagent/models/status_enum.py +12 -0
- hackagent/models/step_type_enum.py +14 -0
- hackagent/models/trace.py +121 -0
- hackagent/models/trace_request.py +94 -0
- hackagent/models/user_api_key.py +201 -0
- hackagent/models/user_api_key_request.py +73 -0
- hackagent/models/user_profile_minimal.py +76 -0
- hackagent/py.typed +1 -0
- hackagent/router/__init__.py +11 -0
- hackagent/router/adapters/__init__.py +5 -0
- hackagent/router/adapters/google_adk.py +658 -0
- hackagent/router/adapters/litellm_adapter.py +290 -0
- hackagent/router/base.py +48 -0
- hackagent/router/router.py +753 -0
- hackagent/types.py +46 -0
- hackagent/utils.py +61 -0
- hackagent/vulnerabilities/__init__.py +0 -0
- hackagent-0.1.0.dist-info/LICENSE +202 -0
- hackagent-0.1.0.dist-info/METADATA +173 -0
- hackagent-0.1.0.dist-info/RECORD +117 -0
- hackagent-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from hackagent.router.base import Agent
|
|
2
|
+
from typing import Any, Dict, Optional, List
|
|
3
|
+
import logging
|
|
4
|
+
import asyncio # Required for async handle_request
|
|
5
|
+
import litellm
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
# from rich.progress import Progress # Removed Progress import
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# --- Custom Exceptions ---
|
|
12
|
+
class LiteLLMConfigurationError(Exception):
|
|
13
|
+
"""Custom exception for LiteLLM adapter configuration issues."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__) # Module-level logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LiteLLMAgentAdapter(Agent):
|
|
22
|
+
"""
|
|
23
|
+
Adapter for interacting with LLMs via the LiteLLM library.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, id: str, config: Dict[str, Any]):
|
|
27
|
+
"""
|
|
28
|
+
Initializes the LiteLLMAgentAdapter.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
id: The unique identifier for this LiteLLM agent instance.
|
|
32
|
+
config: Configuration dictionary for the LiteLLM agent.
|
|
33
|
+
Expected keys:
|
|
34
|
+
- 'name': Model string for LiteLLM (e.g., "ollama/llama3").
|
|
35
|
+
- 'endpoint' (optional): Base URL for the API.
|
|
36
|
+
- 'api_key' (optional): Name of the environment variable holding the API key.
|
|
37
|
+
- 'max_new_tokens' (optional): Default max tokens for generation (defaults to 100).
|
|
38
|
+
- 'temperature' (optional): Default temperature (defaults to 0.8).
|
|
39
|
+
- 'top_p' (optional): Default top_p (defaults to 0.95).
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(id, config)
|
|
42
|
+
self.logger = logging.getLogger(
|
|
43
|
+
f"{__name__}.{self.id}"
|
|
44
|
+
) # Instance-specific logger
|
|
45
|
+
|
|
46
|
+
if "name" not in self.config:
|
|
47
|
+
msg = (
|
|
48
|
+
f"Missing required configuration key 'name' (for model string) for "
|
|
49
|
+
f"LiteLLMAgentAdapter: {self.id}"
|
|
50
|
+
)
|
|
51
|
+
self.logger.error(msg)
|
|
52
|
+
raise LiteLLMConfigurationError(msg)
|
|
53
|
+
|
|
54
|
+
self.model_name: str = self.config["name"]
|
|
55
|
+
self.api_base_url: Optional[str] = self.config.get("endpoint")
|
|
56
|
+
|
|
57
|
+
api_key: Optional[str] = self.config.get("api_key")
|
|
58
|
+
self.actual_api_key: Optional[str] = (
|
|
59
|
+
os.environ.get(api_key) if api_key else None
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.logger.info(
|
|
63
|
+
f"LiteLLMAgentAdapter '{self.id}' initialized for model: '{self.model_name}'"
|
|
64
|
+
+ (f" API Base: '{self.api_base_url}'" if self.api_base_url else "")
|
|
65
|
+
+ (f" API Key: '{api_key}'" if api_key else "")
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Store default generation parameters
|
|
69
|
+
self.default_max_new_tokens = self.config.get("max_new_tokens", 100)
|
|
70
|
+
self.default_temperature = self.config.get("temperature", 0.8)
|
|
71
|
+
self.default_top_p = self.config.get("top_p", 0.95)
|
|
72
|
+
|
|
73
|
+
async def _execute_litellm_completion(
|
|
74
|
+
self,
|
|
75
|
+
texts: List[str],
|
|
76
|
+
max_new_tokens: int,
|
|
77
|
+
temperature: float,
|
|
78
|
+
top_p: float,
|
|
79
|
+
**kwargs,
|
|
80
|
+
) -> List[str]:
|
|
81
|
+
"""
|
|
82
|
+
Internal method to generate completions using litellm.completion.
|
|
83
|
+
"""
|
|
84
|
+
if not texts:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
completions = []
|
|
88
|
+
self.logger.info(
|
|
89
|
+
f"Sending {len(texts)} requests via LiteLLM to model '{self.model_name}'..."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Removed Progress wrapper as it can conflict with outer progress bars
|
|
93
|
+
for text_prompt in texts:
|
|
94
|
+
messages = [{"role": "user", "content": text_prompt}]
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
litellm_params = {
|
|
98
|
+
"model": self.model_name,
|
|
99
|
+
"messages": messages,
|
|
100
|
+
"max_tokens": max_new_tokens,
|
|
101
|
+
"temperature": temperature,
|
|
102
|
+
"top_p": top_p,
|
|
103
|
+
"api_base": self.api_base_url,
|
|
104
|
+
"api_key": self.actual_api_key,
|
|
105
|
+
}
|
|
106
|
+
# Merge any additional kwargs passed directly for litellm.completion
|
|
107
|
+
litellm_params.update(kwargs)
|
|
108
|
+
|
|
109
|
+
# Filter out None values from litellm_params as litellm might not like them for all keys
|
|
110
|
+
# Specifically, api_base and api_key can be None if not provided.
|
|
111
|
+
# LiteLLM handles None for api_base and api_key appropriately.
|
|
112
|
+
# litellm_params = {k: v for k, v in litellm_params.items() if v is not None}
|
|
113
|
+
|
|
114
|
+
max_retries = 3
|
|
115
|
+
retry_delay = 2 # seconds
|
|
116
|
+
for attempt in range(max_retries):
|
|
117
|
+
try:
|
|
118
|
+
response = await asyncio.to_thread(
|
|
119
|
+
litellm.completion, **litellm_params
|
|
120
|
+
)
|
|
121
|
+
# response = litellm.completion(**litellm_params) # original sync call
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
response
|
|
125
|
+
and response.choices
|
|
126
|
+
and response.choices[0].message
|
|
127
|
+
and response.choices[0].message.content
|
|
128
|
+
):
|
|
129
|
+
completion_text = response.choices[0].message.content
|
|
130
|
+
else:
|
|
131
|
+
self.logger.warning(
|
|
132
|
+
f"LiteLLM received unexpected response structure for model '{self.model_name}'. Response: {response}"
|
|
133
|
+
)
|
|
134
|
+
completion_text = " [GENERATION_ERROR: UNEXPECTED_RESPONSE]"
|
|
135
|
+
|
|
136
|
+
full_text = text_prompt + completion_text
|
|
137
|
+
completions.append(full_text)
|
|
138
|
+
break # Success, exit retry loop
|
|
139
|
+
except Exception as e:
|
|
140
|
+
self.logger.warning(
|
|
141
|
+
f"LiteLLM attempt {attempt + 1}/{max_retries} failed for model '{self.model_name}': {e}"
|
|
142
|
+
)
|
|
143
|
+
if attempt + 1 == max_retries:
|
|
144
|
+
self.logger.error(
|
|
145
|
+
f"LiteLLM completion failed after {max_retries} attempts for model '{self.model_name}'.",
|
|
146
|
+
exc_info=True,
|
|
147
|
+
)
|
|
148
|
+
completions.append(
|
|
149
|
+
text_prompt + " [GENERATION_ERROR: MAX_RETRIES]"
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
# time.sleep(retry_delay) # Can't use time.sleep in async directly
|
|
153
|
+
await asyncio.sleep(retry_delay) # Use asyncio.sleep
|
|
154
|
+
except Exception as outer_e:
|
|
155
|
+
self.logger.error(
|
|
156
|
+
f"Critical error during LiteLLM request preparation or retry logic: {outer_e}",
|
|
157
|
+
exc_info=True,
|
|
158
|
+
)
|
|
159
|
+
completions.append(text_prompt + " [GENERATION_ERROR: SETUP_FAILURE]")
|
|
160
|
+
|
|
161
|
+
self.logger.info(
|
|
162
|
+
f"Finished LiteLLM requests for model '{self.model_name}'. Generated {len(completions)} responses."
|
|
163
|
+
)
|
|
164
|
+
return completions
|
|
165
|
+
|
|
166
|
+
async def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
167
|
+
"""
|
|
168
|
+
Handles an incoming request by processing it through LiteLLM.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
request_data: A dictionary containing the request data.
|
|
172
|
+
Expected keys:
|
|
173
|
+
- 'prompt': The text prompt to send to the LLM.
|
|
174
|
+
- 'max_new_tokens' (optional): Override default max tokens.
|
|
175
|
+
- 'temperature' (optional): Override default temperature.
|
|
176
|
+
- 'top_p' (optional): Override default top_p.
|
|
177
|
+
- Any other kwargs to pass to litellm.completion.
|
|
178
|
+
Returns:
|
|
179
|
+
A dictionary representing the agent's response or an error.
|
|
180
|
+
"""
|
|
181
|
+
prompt_text = request_data.get("prompt")
|
|
182
|
+
if not prompt_text:
|
|
183
|
+
self.logger.warning("No 'prompt' found in request_data.")
|
|
184
|
+
return self._build_error_response(
|
|
185
|
+
error_message="Request data must include a 'prompt' field.",
|
|
186
|
+
status_code=400,
|
|
187
|
+
raw_request=request_data,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self.logger.info(
|
|
191
|
+
f"Handling request for LiteLLM adapter {self.id} with prompt: '{prompt_text[:75]}...'"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
max_new_tokens = request_data.get("max_new_tokens", self.default_max_new_tokens)
|
|
195
|
+
temperature = request_data.get("temperature", self.default_temperature)
|
|
196
|
+
top_p = request_data.get("top_p", self.default_top_p)
|
|
197
|
+
|
|
198
|
+
excluded_keys = {"prompt", "max_new_tokens", "temperature", "top_p"}
|
|
199
|
+
additional_kwargs = {
|
|
200
|
+
k: v for k, v in request_data.items() if k not in excluded_keys
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# The _execute_litellm_completion method now handles asyncio.to_thread internally for litellm.completion
|
|
205
|
+
# and also the retry loop with asyncio.sleep
|
|
206
|
+
completions = await self._execute_litellm_completion(
|
|
207
|
+
texts=[prompt_text],
|
|
208
|
+
max_new_tokens=max_new_tokens,
|
|
209
|
+
temperature=temperature,
|
|
210
|
+
top_p=top_p,
|
|
211
|
+
**additional_kwargs,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if completions and isinstance(completions, list) and len(completions) > 0:
|
|
215
|
+
full_response_text = completions[0]
|
|
216
|
+
processed_response = full_response_text
|
|
217
|
+
|
|
218
|
+
if "[GENERATION_ERROR:" in processed_response:
|
|
219
|
+
self.logger.warning(
|
|
220
|
+
f"LiteLLM generation indicated an error for adapter {self.id}: {processed_response}"
|
|
221
|
+
)
|
|
222
|
+
error_detail = processed_response[
|
|
223
|
+
len(prompt_text) :
|
|
224
|
+
].strip() # Get the error part
|
|
225
|
+
return self._build_error_response(
|
|
226
|
+
error_message=f"LiteLLM generation error: {error_detail}",
|
|
227
|
+
status_code=500,
|
|
228
|
+
raw_request=request_data,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
self.logger.info(
|
|
232
|
+
f"Successfully processed request for LiteLLM adapter {self.id}."
|
|
233
|
+
)
|
|
234
|
+
return {
|
|
235
|
+
"raw_request": request_data,
|
|
236
|
+
"processed_response": processed_response,
|
|
237
|
+
"status_code": 200,
|
|
238
|
+
"raw_response_headers": None,
|
|
239
|
+
"raw_response_body": None,
|
|
240
|
+
"agent_specific_data": {
|
|
241
|
+
"model_name": self.model_name, # Updated key
|
|
242
|
+
"invoked_parameters": {
|
|
243
|
+
"max_new_tokens": max_new_tokens,
|
|
244
|
+
"temperature": temperature,
|
|
245
|
+
"top_p": top_p,
|
|
246
|
+
**additional_kwargs,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
"error_message": None,
|
|
250
|
+
"agent_id": self.id,
|
|
251
|
+
"adapter_type": "LiteLLMAgentAdapter",
|
|
252
|
+
}
|
|
253
|
+
else:
|
|
254
|
+
self.logger.error(
|
|
255
|
+
f"LiteLLM returned empty or invalid completions for adapter {self.id}."
|
|
256
|
+
)
|
|
257
|
+
return self._build_error_response(
|
|
258
|
+
error_message="LiteLLM returned empty or invalid result.",
|
|
259
|
+
status_code=500,
|
|
260
|
+
raw_request=request_data,
|
|
261
|
+
)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
self.logger.exception(
|
|
264
|
+
f"Unexpected error in LiteLLMAgentAdapter handle_request for agent {self.id}: {e}"
|
|
265
|
+
)
|
|
266
|
+
return self._build_error_response(
|
|
267
|
+
error_message=f"Unexpected adapter error: {type(e).__name__} - {str(e)}",
|
|
268
|
+
status_code=500,
|
|
269
|
+
raw_request=request_data,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _build_error_response(
|
|
273
|
+
self,
|
|
274
|
+
error_message: str,
|
|
275
|
+
status_code: Optional[int],
|
|
276
|
+
raw_request: Optional[Dict[str, Any]] = None,
|
|
277
|
+
) -> Dict[str, Any]:
|
|
278
|
+
return {
|
|
279
|
+
"raw_request": raw_request,
|
|
280
|
+
"processed_response": None,
|
|
281
|
+
"status_code": status_code if status_code is not None else 500,
|
|
282
|
+
"raw_response_headers": None,
|
|
283
|
+
"raw_response_body": None,
|
|
284
|
+
"agent_specific_data": {
|
|
285
|
+
"model_name": self.model_name if hasattr(self, "model_name") else "N/A"
|
|
286
|
+
}, # Updated key
|
|
287
|
+
"error_message": error_message,
|
|
288
|
+
"agent_id": self.id,
|
|
289
|
+
"adapter_type": "LiteLLMAgentAdapter",
|
|
290
|
+
}
|
hackagent/router/base.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Agent(ABC):
|
|
6
|
+
"""
|
|
7
|
+
Abstract Base Class for all agent implementations.
|
|
8
|
+
It defines a common interface for the router to interact with various agents.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def __init__(self, id: str, config: Dict[str, Any]):
|
|
13
|
+
"""
|
|
14
|
+
Initializes the agent.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
id: A unique identifier for this specific agent instance or type.
|
|
18
|
+
config: Configuration specific to this agent (e.g., API keys, model names).
|
|
19
|
+
"""
|
|
20
|
+
self.id = id
|
|
21
|
+
self.config = config
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Processes an incoming request and returns a standardized response.
|
|
28
|
+
The response should be suitable for storage via the API and should ideally
|
|
29
|
+
include enough information to reconstruct the interaction.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
request_data: The data for the agent to process. This might include
|
|
33
|
+
the prompt, session information, user details, etc.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A dictionary containing the standardized response. This could include:
|
|
37
|
+
- 'raw_request': The original request sent to the underlying agent.
|
|
38
|
+
- 'raw_response': The original response received from the underlying agent.
|
|
39
|
+
- 'processed_response': The key information extracted/processed from the raw response.
|
|
40
|
+
- 'status_code': If applicable, the HTTP status code of the interaction.
|
|
41
|
+
- 'error_message': Any error message encountered.
|
|
42
|
+
- 'metadata': Any other relevant metadata.
|
|
43
|
+
"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def get_identifier(self) -> str:
|
|
47
|
+
"""Returns the unique identifier for this agent instance or type."""
|
|
48
|
+
return self.id
|