ainative-python 2.0.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.
- ainative/__init__.py +46 -0
- ainative/agent_coordination.py +249 -0
- ainative/agent_identity_system.py +1566 -0
- ainative/agent_learning.py +239 -0
- ainative/agent_orchestration.py +202 -0
- ainative/agent_state.py +231 -0
- ainative/agent_swarm/__init__.py +510 -0
- ainative/auth.py +113 -0
- ainative/cli.py +698 -0
- ainative/cli_utils/__init__.py +13 -0
- ainative/cli_utils/diff.py +292 -0
- ainative/cli_utils/formatters.py +227 -0
- ainative/client.py +272 -0
- ainative/commands/__init__.py +28 -0
- ainative/commands/agents.py +238 -0
- ainative/commands/coordination.py +108 -0
- ainative/commands/inspect.py +483 -0
- ainative/commands/learning.py +119 -0
- ainative/commands/local.py +544 -0
- ainative/commands/state.py +144 -0
- ainative/commands/swarm.py +184 -0
- ainative/commands/sync.py +157 -0
- ainative/commands/tasks.py +191 -0
- ainative/exceptions.py +87 -0
- ainative/zerodb/__init__.py +89 -0
- ainative/zerodb/analytics.py +232 -0
- ainative/zerodb/memory.py +260 -0
- ainative/zerodb/projects.py +224 -0
- ainative/zerodb/tables.py +362 -0
- ainative/zerodb/vectors.py +231 -0
- ainative_python-2.0.0.dist-info/METADATA +550 -0
- ainative_python-2.0.0.dist-info/RECORD +35 -0
- ainative_python-2.0.0.dist-info/WHEEL +5 -0
- ainative_python-2.0.0.dist-info/entry_points.txt +2 -0
- ainative_python-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Swarm Module for AINative SDK
|
|
3
|
+
|
|
4
|
+
Provides interface for orchestrating and managing AI agent swarms.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, List, Dict, Any, Optional
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..client import AINativeClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentType(Enum):
|
|
15
|
+
"""Types of agents available in the swarm."""
|
|
16
|
+
RESEARCHER = "researcher"
|
|
17
|
+
CODER = "coder"
|
|
18
|
+
REVIEWER = "reviewer"
|
|
19
|
+
TESTER = "tester"
|
|
20
|
+
DOCUMENTER = "documenter"
|
|
21
|
+
ANALYST = "analyst"
|
|
22
|
+
DESIGNER = "designer"
|
|
23
|
+
ORCHESTRATOR = "orchestrator"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SwarmStatus(Enum):
|
|
27
|
+
"""Status of agent swarm."""
|
|
28
|
+
IDLE = "idle"
|
|
29
|
+
STARTING = "starting"
|
|
30
|
+
RUNNING = "running"
|
|
31
|
+
PAUSED = "paused"
|
|
32
|
+
STOPPING = "stopping"
|
|
33
|
+
COMPLETED = "completed"
|
|
34
|
+
FAILED = "failed"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentSwarmClient:
|
|
38
|
+
"""Main client for Agent Swarm operations."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, client: "AINativeClient"):
|
|
41
|
+
"""
|
|
42
|
+
Initialize Agent Swarm client.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
client: Parent AINative client instance
|
|
46
|
+
"""
|
|
47
|
+
self.client = client
|
|
48
|
+
self.base_path = "/agent-swarm"
|
|
49
|
+
|
|
50
|
+
def start_swarm(
|
|
51
|
+
self,
|
|
52
|
+
project_id: str,
|
|
53
|
+
agents: List[Dict[str, Any]],
|
|
54
|
+
objective: str,
|
|
55
|
+
config: Optional[Dict[str, Any]] = None,
|
|
56
|
+
) -> Dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Start a new agent swarm.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
project_id: Project ID
|
|
62
|
+
agents: List of agent configurations
|
|
63
|
+
objective: Swarm objective/goal
|
|
64
|
+
config: Additional swarm configuration
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Swarm initialization details
|
|
68
|
+
"""
|
|
69
|
+
data = {
|
|
70
|
+
"project_id": project_id,
|
|
71
|
+
"agents": agents,
|
|
72
|
+
"objective": objective,
|
|
73
|
+
"config": config or {},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return self.client.post(f"{self.base_path}/start", data=data)
|
|
77
|
+
|
|
78
|
+
def orchestrate(
|
|
79
|
+
self,
|
|
80
|
+
swarm_id: str,
|
|
81
|
+
task: str,
|
|
82
|
+
context: Optional[Dict[str, Any]] = None,
|
|
83
|
+
agents: Optional[List[str]] = None,
|
|
84
|
+
) -> Dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
Orchestrate agents for a specific task.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
swarm_id: Swarm ID
|
|
90
|
+
task: Task description
|
|
91
|
+
context: Task context
|
|
92
|
+
agents: Specific agents to use (optional)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Orchestration result
|
|
96
|
+
"""
|
|
97
|
+
data = {
|
|
98
|
+
"swarm_id": swarm_id,
|
|
99
|
+
"task": task,
|
|
100
|
+
"context": context or {},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if agents:
|
|
104
|
+
data["agents"] = agents
|
|
105
|
+
|
|
106
|
+
return self.client.post(f"{self.base_path}/orchestrate", data=data)
|
|
107
|
+
|
|
108
|
+
def get_status(self, swarm_id: str) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Get swarm status.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
swarm_id: Swarm ID
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Swarm status details
|
|
117
|
+
"""
|
|
118
|
+
return self.client.get(f"{self.base_path}/{swarm_id}/status")
|
|
119
|
+
|
|
120
|
+
def get_metrics(
|
|
121
|
+
self,
|
|
122
|
+
swarm_id: Optional[str] = None,
|
|
123
|
+
project_id: Optional[str] = None,
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
"""
|
|
126
|
+
Get swarm performance metrics.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
swarm_id: Optional swarm ID filter
|
|
130
|
+
project_id: Optional project ID filter
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Swarm metrics
|
|
134
|
+
"""
|
|
135
|
+
params = {}
|
|
136
|
+
if swarm_id:
|
|
137
|
+
params["swarm_id"] = swarm_id
|
|
138
|
+
if project_id:
|
|
139
|
+
params["project_id"] = project_id
|
|
140
|
+
|
|
141
|
+
return self.client.get(f"{self.base_path}/metrics", params=params)
|
|
142
|
+
|
|
143
|
+
def get_agent_types(self) -> List[Dict[str, Any]]:
|
|
144
|
+
"""
|
|
145
|
+
Get available agent types and their capabilities.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of agent types with descriptions
|
|
149
|
+
"""
|
|
150
|
+
response = self.client.get(f"{self.base_path}/agent-types")
|
|
151
|
+
return response.get("agent_types", [])
|
|
152
|
+
|
|
153
|
+
def configure_agent(
|
|
154
|
+
self,
|
|
155
|
+
swarm_id: str,
|
|
156
|
+
agent_id: str,
|
|
157
|
+
config: Dict[str, Any],
|
|
158
|
+
) -> Dict[str, Any]:
|
|
159
|
+
"""
|
|
160
|
+
Configure a specific agent.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
swarm_id: Swarm ID
|
|
164
|
+
agent_id: Agent ID
|
|
165
|
+
config: Agent configuration
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Configuration result
|
|
169
|
+
"""
|
|
170
|
+
return self.client.put(
|
|
171
|
+
f"{self.base_path}/{swarm_id}/agents/{agent_id}/config",
|
|
172
|
+
data=config
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def set_agent_prompt(
|
|
176
|
+
self,
|
|
177
|
+
swarm_id: str,
|
|
178
|
+
agent_id: str,
|
|
179
|
+
prompt: str,
|
|
180
|
+
system_prompt: Optional[str] = None,
|
|
181
|
+
) -> Dict[str, Any]:
|
|
182
|
+
"""
|
|
183
|
+
Set agent prompt configuration.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
swarm_id: Swarm ID
|
|
187
|
+
agent_id: Agent ID
|
|
188
|
+
prompt: Main prompt
|
|
189
|
+
system_prompt: System prompt (optional)
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Prompt configuration result
|
|
193
|
+
"""
|
|
194
|
+
data = {"prompt": prompt}
|
|
195
|
+
if system_prompt:
|
|
196
|
+
data["system_prompt"] = system_prompt
|
|
197
|
+
|
|
198
|
+
return self.client.post(
|
|
199
|
+
f"{self.base_path}/{swarm_id}/agents/{agent_id}/prompt",
|
|
200
|
+
data=data
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def stop_swarm(self, swarm_id: str, force: bool = False) -> Dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Stop an agent swarm.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
swarm_id: Swarm ID
|
|
209
|
+
force: Force stop without cleanup
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Stop confirmation
|
|
213
|
+
"""
|
|
214
|
+
data = {"force": force}
|
|
215
|
+
return self.client.post(f"{self.base_path}/{swarm_id}/stop", data=data)
|
|
216
|
+
|
|
217
|
+
def pause_swarm(self, swarm_id: str) -> Dict[str, Any]:
|
|
218
|
+
"""
|
|
219
|
+
Pause an agent swarm.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
swarm_id: Swarm ID
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Pause confirmation
|
|
226
|
+
"""
|
|
227
|
+
return self.client.post(f"{self.base_path}/{swarm_id}/pause")
|
|
228
|
+
|
|
229
|
+
def resume_swarm(self, swarm_id: str) -> Dict[str, Any]:
|
|
230
|
+
"""
|
|
231
|
+
Resume a paused swarm.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
swarm_id: Swarm ID
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Resume confirmation
|
|
238
|
+
"""
|
|
239
|
+
return self.client.post(f"{self.base_path}/{swarm_id}/resume")
|
|
240
|
+
|
|
241
|
+
def get_swarm_history(
|
|
242
|
+
self,
|
|
243
|
+
swarm_id: str,
|
|
244
|
+
limit: int = 100,
|
|
245
|
+
) -> List[Dict[str, Any]]:
|
|
246
|
+
"""
|
|
247
|
+
Get swarm execution history.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
swarm_id: Swarm ID
|
|
251
|
+
limit: Maximum number of history entries
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
List of history entries
|
|
255
|
+
"""
|
|
256
|
+
params = {"limit": limit}
|
|
257
|
+
response = self.client.get(
|
|
258
|
+
f"{self.base_path}/{swarm_id}/history",
|
|
259
|
+
params=params
|
|
260
|
+
)
|
|
261
|
+
return response.get("history", [])
|
|
262
|
+
|
|
263
|
+
def get_agent_communications(
|
|
264
|
+
self,
|
|
265
|
+
swarm_id: str,
|
|
266
|
+
agent_id: Optional[str] = None,
|
|
267
|
+
) -> List[Dict[str, Any]]:
|
|
268
|
+
"""
|
|
269
|
+
Get agent communication logs.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
swarm_id: Swarm ID
|
|
273
|
+
agent_id: Optional specific agent ID
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of communication entries
|
|
277
|
+
"""
|
|
278
|
+
params = {}
|
|
279
|
+
if agent_id:
|
|
280
|
+
params["agent_id"] = agent_id
|
|
281
|
+
|
|
282
|
+
response = self.client.get(
|
|
283
|
+
f"{self.base_path}/{swarm_id}/communications",
|
|
284
|
+
params=params
|
|
285
|
+
)
|
|
286
|
+
return response.get("communications", [])
|
|
287
|
+
|
|
288
|
+
def create_agent(
|
|
289
|
+
self,
|
|
290
|
+
name: str,
|
|
291
|
+
agent_type: AgentType,
|
|
292
|
+
capabilities: List[str],
|
|
293
|
+
prompt: str,
|
|
294
|
+
config: Optional[Dict[str, Any]] = None,
|
|
295
|
+
) -> Dict[str, Any]:
|
|
296
|
+
"""
|
|
297
|
+
Create a custom agent template.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
name: Agent name
|
|
301
|
+
agent_type: Type of agent
|
|
302
|
+
capabilities: List of capabilities
|
|
303
|
+
prompt: Agent prompt
|
|
304
|
+
config: Additional configuration
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Created agent details
|
|
308
|
+
"""
|
|
309
|
+
data = {
|
|
310
|
+
"name": name,
|
|
311
|
+
"type": agent_type.value,
|
|
312
|
+
"capabilities": capabilities,
|
|
313
|
+
"prompt": prompt,
|
|
314
|
+
"config": config or {},
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return self.client.post(f"{self.base_path}/agents", data=data)
|
|
318
|
+
|
|
319
|
+
def list_swarms(
|
|
320
|
+
self,
|
|
321
|
+
project_id: Optional[str] = None,
|
|
322
|
+
status: Optional[str] = None,
|
|
323
|
+
limit: int = 100,
|
|
324
|
+
offset: int = 0,
|
|
325
|
+
) -> Dict[str, Any]:
|
|
326
|
+
"""
|
|
327
|
+
List all swarms with optional filtering.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
project_id: Filter by project ID
|
|
331
|
+
status: Filter by status
|
|
332
|
+
limit: Maximum number of results
|
|
333
|
+
offset: Pagination offset
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
List of swarms with pagination metadata
|
|
337
|
+
"""
|
|
338
|
+
params = {
|
|
339
|
+
"limit": limit,
|
|
340
|
+
"offset": offset,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if project_id:
|
|
344
|
+
params["project_id"] = project_id
|
|
345
|
+
if status:
|
|
346
|
+
params["status"] = status
|
|
347
|
+
|
|
348
|
+
return self.client.get(self.base_path, params=params)
|
|
349
|
+
|
|
350
|
+
def delete_swarm(self, swarm_id: str, force: bool = False) -> Dict[str, Any]:
|
|
351
|
+
"""
|
|
352
|
+
Delete a swarm.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
swarm_id: Swarm ID to delete
|
|
356
|
+
force: Force deletion without cleanup
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Deletion confirmation
|
|
360
|
+
"""
|
|
361
|
+
params = {"force": str(force).lower()}
|
|
362
|
+
return self.client.delete(f"{self.base_path}/{swarm_id}", params=params)
|
|
363
|
+
|
|
364
|
+
def scale_swarm(
|
|
365
|
+
self,
|
|
366
|
+
swarm_id: str,
|
|
367
|
+
agent_counts: Dict[str, int],
|
|
368
|
+
) -> Dict[str, Any]:
|
|
369
|
+
"""
|
|
370
|
+
Scale swarm by adjusting agent counts.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
swarm_id: Swarm ID
|
|
374
|
+
agent_counts: Dictionary mapping agent types to desired counts
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Scaling operation result
|
|
378
|
+
"""
|
|
379
|
+
data = {"agent_counts": agent_counts}
|
|
380
|
+
return self.client.post(f"{self.base_path}/{swarm_id}/scale", data=data)
|
|
381
|
+
|
|
382
|
+
def get_analytics(
|
|
383
|
+
self,
|
|
384
|
+
swarm_id: str,
|
|
385
|
+
metric_types: Optional[List[str]] = None,
|
|
386
|
+
time_range: str = "7d",
|
|
387
|
+
) -> Dict[str, Any]:
|
|
388
|
+
"""
|
|
389
|
+
Get swarm analytics and performance metrics.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
swarm_id: Swarm ID
|
|
393
|
+
metric_types: Specific metrics to retrieve
|
|
394
|
+
time_range: Time range (1d, 7d, 30d, all)
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Analytics data
|
|
398
|
+
"""
|
|
399
|
+
params = {"time_range": time_range}
|
|
400
|
+
|
|
401
|
+
if metric_types:
|
|
402
|
+
params["metric_types"] = ",".join(metric_types)
|
|
403
|
+
|
|
404
|
+
return self.client.get(f"{self.base_path}/{swarm_id}/analytics", params=params)
|
|
405
|
+
|
|
406
|
+
def execute_parallel_tasks(
|
|
407
|
+
self,
|
|
408
|
+
swarm_id: str,
|
|
409
|
+
tasks: List[Dict[str, Any]],
|
|
410
|
+
max_concurrency: Optional[int] = None,
|
|
411
|
+
) -> Dict[str, Any]:
|
|
412
|
+
"""
|
|
413
|
+
Execute multiple tasks in parallel across swarm.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
swarm_id: Swarm ID
|
|
417
|
+
tasks: List of task definitions
|
|
418
|
+
max_concurrency: Maximum concurrent executions
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Parallel execution results
|
|
422
|
+
"""
|
|
423
|
+
data = {
|
|
424
|
+
"tasks": tasks,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if max_concurrency:
|
|
428
|
+
data["max_concurrency"] = max_concurrency
|
|
429
|
+
|
|
430
|
+
return self.client.post(
|
|
431
|
+
f"{self.base_path}/{swarm_id}/tasks/parallel",
|
|
432
|
+
data=data
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def get_swarm_health(self, swarm_id: str) -> Dict[str, Any]:
|
|
436
|
+
"""
|
|
437
|
+
Get swarm health status.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
swarm_id: Swarm ID
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Health status details
|
|
444
|
+
"""
|
|
445
|
+
return self.client.get(f"{self.base_path}/{swarm_id}/health")
|
|
446
|
+
|
|
447
|
+
def update_swarm_config(
|
|
448
|
+
self,
|
|
449
|
+
swarm_id: str,
|
|
450
|
+
config: Dict[str, Any],
|
|
451
|
+
) -> Dict[str, Any]:
|
|
452
|
+
"""
|
|
453
|
+
Update swarm configuration.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
swarm_id: Swarm ID
|
|
457
|
+
config: New configuration settings
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Updated configuration
|
|
461
|
+
"""
|
|
462
|
+
return self.client.put(f"{self.base_path}/{swarm_id}/config", data=config)
|
|
463
|
+
|
|
464
|
+
def get_agent_status(
|
|
465
|
+
self,
|
|
466
|
+
swarm_id: str,
|
|
467
|
+
agent_id: str,
|
|
468
|
+
) -> Dict[str, Any]:
|
|
469
|
+
"""
|
|
470
|
+
Get detailed status of a specific agent in the swarm.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
swarm_id: Swarm ID
|
|
474
|
+
agent_id: Agent ID
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Agent status details
|
|
478
|
+
"""
|
|
479
|
+
return self.client.get(f"{self.base_path}/{swarm_id}/agents/{agent_id}/status")
|
|
480
|
+
|
|
481
|
+
def broadcast_message(
|
|
482
|
+
self,
|
|
483
|
+
swarm_id: str,
|
|
484
|
+
message: str,
|
|
485
|
+
target_agents: Optional[List[str]] = None,
|
|
486
|
+
) -> Dict[str, Any]:
|
|
487
|
+
"""
|
|
488
|
+
Broadcast a message to all or specific agents in swarm.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
swarm_id: Swarm ID
|
|
492
|
+
message: Message to broadcast
|
|
493
|
+
target_agents: Optional list of specific agent IDs
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Broadcast confirmation
|
|
497
|
+
"""
|
|
498
|
+
data = {"message": message}
|
|
499
|
+
|
|
500
|
+
if target_agents:
|
|
501
|
+
data["target_agents"] = target_agents
|
|
502
|
+
|
|
503
|
+
return self.client.post(f"{self.base_path}/{swarm_id}/broadcast", data=data)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
__all__ = [
|
|
507
|
+
"AgentSwarmClient",
|
|
508
|
+
"AgentType",
|
|
509
|
+
"SwarmStatus",
|
|
510
|
+
]
|
ainative/auth.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AINative SDK Authentication Module
|
|
3
|
+
|
|
4
|
+
Handles API key authentication and authorization for AINative Studio APIs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import time
|
|
11
|
+
import hashlib
|
|
12
|
+
import hmac
|
|
13
|
+
import base64
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AuthConfig:
|
|
18
|
+
"""Configuration for authentication."""
|
|
19
|
+
|
|
20
|
+
api_key: Optional[str] = None
|
|
21
|
+
api_secret: Optional[str] = None
|
|
22
|
+
environment: str = "production"
|
|
23
|
+
auto_refresh: bool = True
|
|
24
|
+
timeout: int = 30
|
|
25
|
+
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
"""Load from environment variables if not provided."""
|
|
28
|
+
if not self.api_key:
|
|
29
|
+
self.api_key = os.getenv("AINATIVE_API_KEY")
|
|
30
|
+
if not self.api_secret:
|
|
31
|
+
self.api_secret = os.getenv("AINATIVE_API_SECRET")
|
|
32
|
+
|
|
33
|
+
# Validate environment
|
|
34
|
+
valid_environments = ["production", "staging", "development", "local"]
|
|
35
|
+
if self.environment not in valid_environments:
|
|
36
|
+
raise ValueError(f"Invalid environment: {self.environment}")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_configured(self) -> bool:
|
|
40
|
+
"""Check if authentication is properly configured."""
|
|
41
|
+
return bool(self.api_key)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class APIKeyAuth:
|
|
45
|
+
"""Handles API key authentication for requests."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, config: AuthConfig):
|
|
48
|
+
self.config = config
|
|
49
|
+
self._token_cache: Optional[Dict[str, Any]] = None
|
|
50
|
+
self._token_expiry: float = 0
|
|
51
|
+
|
|
52
|
+
def get_headers(self) -> Dict[str, str]:
|
|
53
|
+
"""Get authentication headers for API requests."""
|
|
54
|
+
if not self.config.api_key:
|
|
55
|
+
raise ValueError("API key not configured")
|
|
56
|
+
|
|
57
|
+
headers = {
|
|
58
|
+
"X-API-Key": self.config.api_key,
|
|
59
|
+
"X-SDK-Version": "0.1.0",
|
|
60
|
+
"X-SDK-Language": "Python",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Add signature if API secret is provided
|
|
64
|
+
if self.config.api_secret:
|
|
65
|
+
timestamp = str(int(time.time()))
|
|
66
|
+
signature = self._generate_signature(timestamp)
|
|
67
|
+
headers.update({
|
|
68
|
+
"X-Timestamp": timestamp,
|
|
69
|
+
"X-Signature": signature,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return headers
|
|
73
|
+
|
|
74
|
+
def _generate_signature(self, timestamp: str) -> str:
|
|
75
|
+
"""Generate HMAC signature for request."""
|
|
76
|
+
message = f"{self.config.api_key}{timestamp}"
|
|
77
|
+
signature = hmac.new(
|
|
78
|
+
self.config.api_secret.encode(),
|
|
79
|
+
message.encode(),
|
|
80
|
+
hashlib.sha256
|
|
81
|
+
).digest()
|
|
82
|
+
return base64.b64encode(signature).decode()
|
|
83
|
+
|
|
84
|
+
def get_bearer_token(self) -> Optional[str]:
|
|
85
|
+
"""Get Bearer token if using OAuth flow (future enhancement)."""
|
|
86
|
+
# Placeholder for future OAuth implementation
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def validate_credentials(self) -> bool:
|
|
90
|
+
"""Validate that credentials are properly configured."""
|
|
91
|
+
return self.config.is_configured
|
|
92
|
+
|
|
93
|
+
def refresh_token(self) -> bool:
|
|
94
|
+
"""Refresh authentication token if needed (future enhancement)."""
|
|
95
|
+
# Placeholder for future token refresh logic
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class MultiTenantAuth(APIKeyAuth):
|
|
100
|
+
"""Extended authentication for multi-tenant scenarios."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, config: AuthConfig, organization_id: Optional[str] = None):
|
|
103
|
+
super().__init__(config)
|
|
104
|
+
self.organization_id = organization_id or os.getenv("AINATIVE_ORG_ID")
|
|
105
|
+
|
|
106
|
+
def get_headers(self) -> Dict[str, str]:
|
|
107
|
+
"""Get headers including organization context."""
|
|
108
|
+
headers = super().get_headers()
|
|
109
|
+
|
|
110
|
+
if self.organization_id:
|
|
111
|
+
headers["X-Organization-ID"] = self.organization_id
|
|
112
|
+
|
|
113
|
+
return headers
|