daita-agents 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.

Potentially problematic release.


This version of daita-agents might be problematic. Click here for more details.

Files changed (69) hide show
  1. daita/__init__.py +208 -0
  2. daita/agents/__init__.py +33 -0
  3. daita/agents/base.py +722 -0
  4. daita/agents/substrate.py +895 -0
  5. daita/cli/__init__.py +145 -0
  6. daita/cli/__main__.py +7 -0
  7. daita/cli/ascii_art.py +44 -0
  8. daita/cli/core/__init__.py +0 -0
  9. daita/cli/core/create.py +254 -0
  10. daita/cli/core/deploy.py +473 -0
  11. daita/cli/core/deployments.py +309 -0
  12. daita/cli/core/import_detector.py +219 -0
  13. daita/cli/core/init.py +382 -0
  14. daita/cli/core/logs.py +239 -0
  15. daita/cli/core/managed_deploy.py +709 -0
  16. daita/cli/core/run.py +648 -0
  17. daita/cli/core/status.py +421 -0
  18. daita/cli/core/test.py +239 -0
  19. daita/cli/core/webhooks.py +172 -0
  20. daita/cli/main.py +588 -0
  21. daita/cli/utils.py +541 -0
  22. daita/config/__init__.py +62 -0
  23. daita/config/base.py +159 -0
  24. daita/config/settings.py +184 -0
  25. daita/core/__init__.py +262 -0
  26. daita/core/decision_tracing.py +701 -0
  27. daita/core/exceptions.py +480 -0
  28. daita/core/focus.py +251 -0
  29. daita/core/interfaces.py +76 -0
  30. daita/core/plugin_tracing.py +550 -0
  31. daita/core/relay.py +695 -0
  32. daita/core/reliability.py +381 -0
  33. daita/core/scaling.py +444 -0
  34. daita/core/tools.py +402 -0
  35. daita/core/tracing.py +770 -0
  36. daita/core/workflow.py +1084 -0
  37. daita/display/__init__.py +1 -0
  38. daita/display/console.py +160 -0
  39. daita/execution/__init__.py +58 -0
  40. daita/execution/client.py +856 -0
  41. daita/execution/exceptions.py +92 -0
  42. daita/execution/models.py +317 -0
  43. daita/llm/__init__.py +60 -0
  44. daita/llm/anthropic.py +166 -0
  45. daita/llm/base.py +373 -0
  46. daita/llm/factory.py +101 -0
  47. daita/llm/gemini.py +152 -0
  48. daita/llm/grok.py +114 -0
  49. daita/llm/mock.py +135 -0
  50. daita/llm/openai.py +109 -0
  51. daita/plugins/__init__.py +141 -0
  52. daita/plugins/base.py +37 -0
  53. daita/plugins/base_db.py +167 -0
  54. daita/plugins/elasticsearch.py +844 -0
  55. daita/plugins/mcp.py +481 -0
  56. daita/plugins/mongodb.py +510 -0
  57. daita/plugins/mysql.py +351 -0
  58. daita/plugins/postgresql.py +331 -0
  59. daita/plugins/redis_messaging.py +500 -0
  60. daita/plugins/rest.py +529 -0
  61. daita/plugins/s3.py +761 -0
  62. daita/plugins/slack.py +729 -0
  63. daita/utils/__init__.py +18 -0
  64. daita_agents-0.1.0.dist-info/METADATA +350 -0
  65. daita_agents-0.1.0.dist-info/RECORD +69 -0
  66. daita_agents-0.1.0.dist-info/WHEEL +5 -0
  67. daita_agents-0.1.0.dist-info/entry_points.txt +2 -0
  68. daita_agents-0.1.0.dist-info/licenses/LICENSE +56 -0
  69. daita_agents-0.1.0.dist-info/top_level.txt +1 -0
daita/core/scaling.py ADDED
@@ -0,0 +1,444 @@
1
+ """
2
+ Agent Pool Scaling for Daita Agents.
3
+
4
+ Provides manual horizontal scaling of agent instances for handling concurrent workloads.
5
+ Uses a simple, MVP-focused approach without complex auto-scaling logic.
6
+
7
+ Features:
8
+ - Manual agent pool management
9
+ - Round-robin load balancing
10
+ - Simple instance lifecycle management
11
+ - Integration with existing reliability features
12
+ - Async-safe concurrent task execution
13
+
14
+ Example:
15
+ ```python
16
+ from daita.core.scaling import AgentPool
17
+ from daita.agents.substrate import SubstrateAgent
18
+
19
+ # Create agent factory
20
+ def create_processor():
21
+ return SubstrateAgent(name="Processor", preset="analysis")
22
+
23
+ # Create agent pool with 5 instances
24
+ pool = AgentPool(
25
+ agent_factory=create_processor,
26
+ instances=5,
27
+ pool_name="processors"
28
+ )
29
+
30
+ await pool.start()
31
+
32
+ # Submit tasks to pool (load balanced)
33
+ result = await pool.submit_task("analyze", data={"text": "Hello"})
34
+
35
+ await pool.stop()
36
+ ```
37
+ """
38
+
39
+ import asyncio
40
+ import logging
41
+ import time
42
+ import uuid
43
+ from typing import Dict, Any, Optional, List, Callable, Union
44
+ from dataclasses import dataclass, field
45
+ from enum import Enum
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ class PoolStatus(str, Enum):
50
+ """Status of an agent pool."""
51
+ CREATED = "created"
52
+ STARTING = "starting"
53
+ RUNNING = "running"
54
+ STOPPING = "stopping"
55
+ STOPPED = "stopped"
56
+ ERROR = "error"
57
+
58
+ @dataclass
59
+ class AgentInstance:
60
+ """Represents an agent instance in a pool."""
61
+ id: str
62
+ agent: Any
63
+ created_at: float = field(default_factory=time.time)
64
+ task_count: int = 0
65
+ last_task_at: Optional[float] = None
66
+ current_tasks: int = 0
67
+ status: str = "idle"
68
+
69
+ def is_available(self) -> bool:
70
+ """Check if instance is available for new tasks."""
71
+ return self.status == "idle" and self.current_tasks == 0
72
+
73
+ class LoadBalancer:
74
+ """Simple round-robin load balancer for agent instances."""
75
+
76
+ def __init__(self):
77
+ self.current_index = 0
78
+
79
+ def select_instance(self, instances: List[AgentInstance]) -> Optional[AgentInstance]:
80
+ """
81
+ Select next available agent instance using round-robin.
82
+
83
+ Args:
84
+ instances: List of agent instances
85
+
86
+ Returns:
87
+ Selected agent instance or None if none available
88
+ """
89
+ if not instances:
90
+ return None
91
+
92
+ # Try round-robin selection first
93
+ available_instances = [inst for inst in instances if inst.is_available()]
94
+
95
+ if not available_instances:
96
+ return None
97
+
98
+ # Select using round-robin
99
+ selected = available_instances[self.current_index % len(available_instances)]
100
+ self.current_index += 1
101
+
102
+ return selected
103
+
104
+ class AgentPool:
105
+ """
106
+ Agent pool for horizontal scaling with manual instance management.
107
+
108
+ Provides load balancing across multiple agent instances to handle
109
+ concurrent workloads. Uses simple round-robin balancing and manual
110
+ instance count management.
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ agent_factory: Callable[[], Any],
116
+ instances: int = 1,
117
+ pool_name: Optional[str] = None,
118
+ max_concurrent_per_instance: int = 1
119
+ ):
120
+ """
121
+ Initialize agent pool.
122
+
123
+ Args:
124
+ agent_factory: Factory function to create agent instances
125
+ instances: Number of agent instances to create
126
+ pool_name: Optional name for the pool (for logging)
127
+ max_concurrent_per_instance: Max concurrent tasks per agent (default: 1)
128
+ """
129
+ self.agent_factory = agent_factory
130
+ self.instance_count = max(1, instances) # At least 1 instance
131
+ self.pool_name = pool_name or f"pool_{uuid.uuid4().hex[:8]}"
132
+ self.max_concurrent_per_instance = max_concurrent_per_instance
133
+
134
+ # Pool state
135
+ self.status = PoolStatus.CREATED
136
+ self.instances: List[AgentInstance] = []
137
+ self.load_balancer = LoadBalancer()
138
+
139
+ # Statistics
140
+ self.total_tasks = 0
141
+ self.failed_tasks = 0
142
+ self.created_at = time.time()
143
+
144
+ # Async locks
145
+ self._pool_lock = asyncio.Lock()
146
+
147
+ logger.debug(f"AgentPool '{self.pool_name}' created with {self.instance_count} instances")
148
+
149
+ async def start(self) -> None:
150
+ """Start the agent pool and create all agent instances."""
151
+ if self.status != PoolStatus.CREATED:
152
+ logger.warning(f"Pool '{self.pool_name}' already started or in invalid state")
153
+ return
154
+
155
+ self.status = PoolStatus.STARTING
156
+ logger.info(f"Starting agent pool '{self.pool_name}' with {self.instance_count} instances")
157
+
158
+ try:
159
+ async with self._pool_lock:
160
+ # Create all agent instances
161
+ for i in range(self.instance_count):
162
+ await self._create_instance(f"{self.pool_name}_instance_{i}")
163
+
164
+ self.status = PoolStatus.RUNNING
165
+ logger.info(f"Agent pool '{self.pool_name}' started successfully")
166
+
167
+ except Exception as e:
168
+ self.status = PoolStatus.ERROR
169
+ logger.error(f"Failed to start agent pool '{self.pool_name}': {e}")
170
+ raise
171
+
172
+ async def stop(self) -> None:
173
+ """Stop the agent pool and cleanup all instances."""
174
+ if self.status in [PoolStatus.STOPPED, PoolStatus.STOPPING]:
175
+ return
176
+
177
+ self.status = PoolStatus.STOPPING
178
+ logger.info(f"Stopping agent pool '{self.pool_name}'")
179
+
180
+ try:
181
+ async with self._pool_lock:
182
+ # Stop all agent instances
183
+ for instance in self.instances:
184
+ try:
185
+ if hasattr(instance.agent, 'stop'):
186
+ await instance.agent.stop()
187
+ except Exception as e:
188
+ logger.warning(f"Error stopping instance {instance.id}: {e}")
189
+
190
+ self.instances.clear()
191
+
192
+ self.status = PoolStatus.STOPPED
193
+ logger.info(f"Agent pool '{self.pool_name}' stopped")
194
+
195
+ except Exception as e:
196
+ self.status = PoolStatus.ERROR
197
+ logger.error(f"Error stopping agent pool '{self.pool_name}': {e}")
198
+ raise
199
+
200
+ async def _create_instance(self, instance_id: str) -> AgentInstance:
201
+ """
202
+ Create and start a new agent instance.
203
+
204
+ Args:
205
+ instance_id: Unique ID for the instance
206
+
207
+ Returns:
208
+ Created agent instance
209
+ """
210
+ try:
211
+ # Create agent using factory
212
+ agent = self.agent_factory()
213
+
214
+ # Start the agent if it has a start method
215
+ if hasattr(agent, 'start'):
216
+ await agent.start()
217
+
218
+ # Create instance record
219
+ instance = AgentInstance(
220
+ id=instance_id,
221
+ agent=agent
222
+ )
223
+
224
+ self.instances.append(instance)
225
+ logger.debug(f"Created agent instance {instance_id}")
226
+
227
+ return instance
228
+
229
+ except Exception as e:
230
+ logger.error(f"Failed to create agent instance {instance_id}: {e}")
231
+ raise
232
+
233
+ async def submit_task(
234
+ self,
235
+ task: str,
236
+ data: Any = None,
237
+ context: Optional[Dict[str, Any]] = None,
238
+ **kwargs
239
+ ) -> Any:
240
+ """
241
+ Submit task to an available agent instance.
242
+
243
+ Args:
244
+ task: Task name/type
245
+ data: Task data
246
+ context: Optional task context
247
+ **kwargs: Additional task parameters
248
+
249
+ Returns:
250
+ Task result
251
+ """
252
+ if self.status != PoolStatus.RUNNING:
253
+ raise RuntimeError(f"Pool '{self.pool_name}' is not running (status: {self.status})")
254
+
255
+ # Select available instance
256
+ instance = self.load_balancer.select_instance(self.instances)
257
+ if not instance:
258
+ raise RuntimeError(f"No available agent instances in pool '{self.pool_name}'")
259
+
260
+ # Track task execution
261
+ self.total_tasks += 1
262
+ instance.task_count += 1
263
+ instance.current_tasks += 1
264
+ instance.last_task_at = time.time()
265
+ instance.status = "busy"
266
+
267
+ try:
268
+ # Execute task on selected agent
269
+ logger.debug(f"Submitting task '{task}' to instance {instance.id}")
270
+
271
+ # Call agent's process method
272
+ result = await instance.agent.process(task, data, context or {}, **kwargs)
273
+
274
+ logger.debug(f"Task '{task}' completed on instance {instance.id}")
275
+ return result
276
+
277
+ except Exception as e:
278
+ self.failed_tasks += 1
279
+ logger.error(f"Task '{task}' failed on instance {instance.id}: {e}")
280
+ raise
281
+ finally:
282
+ # Update instance state
283
+ instance.current_tasks = max(0, instance.current_tasks - 1)
284
+ if instance.current_tasks == 0:
285
+ instance.status = "idle"
286
+
287
+ async def resize(self, new_instance_count: int) -> None:
288
+ """
289
+ Resize the agent pool (manual scaling).
290
+
291
+ Args:
292
+ new_instance_count: New number of instances
293
+ """
294
+ if self.status != PoolStatus.RUNNING:
295
+ raise RuntimeError(f"Cannot resize pool '{self.pool_name}' - not running")
296
+
297
+ new_instance_count = max(1, new_instance_count) # At least 1 instance
298
+ current_count = len(self.instances)
299
+
300
+ if new_instance_count == current_count:
301
+ logger.debug(f"Pool '{self.pool_name}' already has {current_count} instances")
302
+ return
303
+
304
+ async with self._pool_lock:
305
+ if new_instance_count > current_count:
306
+ # Scale up - add instances
307
+ instances_to_add = new_instance_count - current_count
308
+ logger.info(f"Scaling up pool '{self.pool_name}' from {current_count} to {new_instance_count} instances")
309
+
310
+ for i in range(instances_to_add):
311
+ instance_id = f"{self.pool_name}_instance_{current_count + i}"
312
+ await self._create_instance(instance_id)
313
+
314
+ else:
315
+ # Scale down - remove instances
316
+ instances_to_remove = current_count - new_instance_count
317
+ logger.info(f"Scaling down pool '{self.pool_name}' from {current_count} to {new_instance_count} instances")
318
+
319
+ # Remove least busy instances
320
+ instances_by_load = sorted(self.instances, key=lambda x: x.current_tasks)
321
+
322
+ for _ in range(instances_to_remove):
323
+ if instances_by_load:
324
+ instance = instances_by_load.pop(0)
325
+
326
+ # Wait for current tasks to complete (with timeout)
327
+ timeout_seconds = 30
328
+ wait_start = time.time()
329
+
330
+ while instance.current_tasks > 0 and (time.time() - wait_start) < timeout_seconds:
331
+ await asyncio.sleep(0.1)
332
+
333
+ # Stop and remove instance
334
+ try:
335
+ if hasattr(instance.agent, 'stop'):
336
+ await instance.agent.stop()
337
+ except Exception as e:
338
+ logger.warning(f"Error stopping instance {instance.id}: {e}")
339
+
340
+ self.instances.remove(instance)
341
+ logger.debug(f"Removed instance {instance.id}")
342
+
343
+ self.instance_count = new_instance_count
344
+ logger.info(f"Pool '{self.pool_name}' resized to {new_instance_count} instances")
345
+
346
+ def get_stats(self) -> Dict[str, Any]:
347
+ """Get agent pool statistics."""
348
+ if not self.instances:
349
+ return {
350
+ "pool_name": self.pool_name,
351
+ "status": self.status.value,
352
+ "instance_count": 0,
353
+ "total_tasks": self.total_tasks,
354
+ "failed_tasks": self.failed_tasks,
355
+ "success_rate": 0.0,
356
+ "uptime_seconds": time.time() - self.created_at
357
+ }
358
+
359
+ # Calculate instance statistics
360
+ busy_instances = sum(1 for inst in self.instances if inst.current_tasks > 0)
361
+ total_current_tasks = sum(inst.current_tasks for inst in self.instances)
362
+ avg_tasks_per_instance = sum(inst.task_count for inst in self.instances) / len(self.instances)
363
+
364
+ success_rate = 0.0
365
+ if self.total_tasks > 0:
366
+ success_rate = ((self.total_tasks - self.failed_tasks) / self.total_tasks) * 100
367
+
368
+ return {
369
+ "pool_name": self.pool_name,
370
+ "status": self.status.value,
371
+ "instance_count": len(self.instances),
372
+ "busy_instances": busy_instances,
373
+ "idle_instances": len(self.instances) - busy_instances,
374
+ "total_current_tasks": total_current_tasks,
375
+ "total_tasks": self.total_tasks,
376
+ "failed_tasks": self.failed_tasks,
377
+ "success_rate": round(success_rate, 2),
378
+ "avg_tasks_per_instance": round(avg_tasks_per_instance, 2),
379
+ "uptime_seconds": round(time.time() - self.created_at, 2)
380
+ }
381
+
382
+ def get_instance_stats(self) -> List[Dict[str, Any]]:
383
+ """Get detailed statistics for each instance."""
384
+ return [
385
+ {
386
+ "id": inst.id,
387
+ "status": inst.status,
388
+ "task_count": inst.task_count,
389
+ "current_tasks": inst.current_tasks,
390
+ "last_task_at": inst.last_task_at,
391
+ "uptime_seconds": round(time.time() - inst.created_at, 2),
392
+ "is_available": inst.is_available()
393
+ }
394
+ for inst in self.instances
395
+ ]
396
+
397
+ # Context manager support
398
+ async def __aenter__(self) -> "AgentPool":
399
+ """Async context manager entry."""
400
+ await self.start()
401
+ return self
402
+
403
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
404
+ """Async context manager exit."""
405
+ await self.stop()
406
+
407
+ # Utility functions for pool management
408
+
409
+ def create_agent_pool(
410
+ agent_factory: Callable[[], Any],
411
+ instances: int = 1,
412
+ pool_name: Optional[str] = None
413
+ ) -> AgentPool:
414
+ """
415
+ Create an agent pool with the specified configuration.
416
+
417
+ Args:
418
+ agent_factory: Factory function to create agent instances
419
+ instances: Number of agent instances
420
+ pool_name: Optional pool name
421
+
422
+ Returns:
423
+ Configured AgentPool instance
424
+
425
+ Example:
426
+ ```python
427
+ from daita.core.scaling import create_agent_pool
428
+ from daita.agents.substrate import SubstrateAgent
429
+
430
+ # Create pool factory
431
+ def make_processor():
432
+ return SubstrateAgent(name="Processor")
433
+
434
+ pool = create_agent_pool(make_processor, instances=5, pool_name="processors")
435
+
436
+ async with pool:
437
+ result = await pool.submit_task("analyze", data={"text": "Hello"})
438
+ ```
439
+ """
440
+ return AgentPool(
441
+ agent_factory=agent_factory,
442
+ instances=instances,
443
+ pool_name=pool_name
444
+ )