daita-agents 0.2.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.
Files changed (69) hide show
  1. daita/__init__.py +216 -0
  2. daita/agents/__init__.py +33 -0
  3. daita/agents/base.py +743 -0
  4. daita/agents/substrate.py +1141 -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 +481 -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 +779 -0
  32. daita/core/reliability.py +381 -0
  33. daita/core/scaling.py +459 -0
  34. daita/core/tools.py +554 -0
  35. daita/core/tracing.py +770 -0
  36. daita/core/workflow.py +1144 -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 +291 -0
  45. daita/llm/base.py +530 -0
  46. daita/llm/factory.py +101 -0
  47. daita/llm/gemini.py +355 -0
  48. daita/llm/grok.py +219 -0
  49. daita/llm/mock.py +172 -0
  50. daita/llm/openai.py +220 -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 +849 -0
  55. daita/plugins/mcp.py +481 -0
  56. daita/plugins/mongodb.py +520 -0
  57. daita/plugins/mysql.py +362 -0
  58. daita/plugins/postgresql.py +342 -0
  59. daita/plugins/redis_messaging.py +500 -0
  60. daita/plugins/rest.py +537 -0
  61. daita/plugins/s3.py +770 -0
  62. daita/plugins/slack.py +729 -0
  63. daita/utils/__init__.py +18 -0
  64. daita_agents-0.2.0.dist-info/METADATA +409 -0
  65. daita_agents-0.2.0.dist-info/RECORD +69 -0
  66. daita_agents-0.2.0.dist-info/WHEEL +5 -0
  67. daita_agents-0.2.0.dist-info/entry_points.txt +2 -0
  68. daita_agents-0.2.0.dist-info/licenses/LICENSE +56 -0
  69. daita_agents-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,849 @@
1
+ """
2
+ Elasticsearch plugin for Daita Agents.
3
+
4
+ Simple Elasticsearch search and indexing - no over-engineering.
5
+ """
6
+ import logging
7
+ import json
8
+ import asyncio
9
+ from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING
10
+ from datetime import datetime
11
+
12
+ if TYPE_CHECKING:
13
+ from ..core.tools import AgentTool
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class ElasticsearchPlugin:
18
+ """
19
+ Simple Elasticsearch plugin for agents.
20
+
21
+ Handles search, indexing, and analytics with focus system support and agent-specific features.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ hosts: Union[str, List[str]] = "localhost:9200",
27
+ auth_method: str = "basic",
28
+ username: Optional[str] = None,
29
+ password: Optional[str] = None,
30
+ api_key_id: Optional[str] = None,
31
+ api_key: Optional[str] = None,
32
+ ssl_fingerprint: Optional[str] = None,
33
+ verify_certs: bool = True,
34
+ ca_certs: Optional[str] = None,
35
+ timeout: int = 30,
36
+ max_retries: int = 3,
37
+ **kwargs
38
+ ):
39
+ """
40
+ Initialize Elasticsearch connection.
41
+
42
+ Args:
43
+ hosts: Elasticsearch host(s) - string or list of hosts
44
+ auth_method: Authentication method ('basic', 'api_key', 'ssl')
45
+ username: Username for basic auth
46
+ password: Password for basic auth
47
+ api_key_id: API key ID for API key auth
48
+ api_key: API key for API key auth
49
+ ssl_fingerprint: SSL certificate fingerprint
50
+ verify_certs: Whether to verify SSL certificates
51
+ ca_certs: Path to CA certificates file
52
+ timeout: Request timeout in seconds
53
+ max_retries: Maximum number of retries
54
+ **kwargs: Additional Elasticsearch client parameters
55
+ """
56
+ # Normalize hosts to list
57
+ if isinstance(hosts, str):
58
+ self.hosts = [hosts]
59
+ else:
60
+ self.hosts = hosts
61
+
62
+ if not self.hosts:
63
+ raise ValueError("At least one Elasticsearch host must be specified")
64
+
65
+ self.auth_method = auth_method
66
+ self.username = username
67
+ self.password = password
68
+ self.api_key_id = api_key_id
69
+ self.api_key = api_key
70
+ self.ssl_fingerprint = ssl_fingerprint
71
+ self.verify_certs = verify_certs
72
+ self.ca_certs = ca_certs
73
+ self.timeout = timeout
74
+ self.max_retries = max_retries
75
+
76
+ # Store additional config
77
+ self.config = kwargs
78
+
79
+ self._client = None
80
+ self._cluster_info = None
81
+
82
+ logger.debug(f"Elasticsearch plugin configured for hosts: {self.hosts}")
83
+
84
+ async def connect(self):
85
+ """Initialize Elasticsearch client and test connection."""
86
+ if self._client is not None:
87
+ return # Already connected
88
+
89
+ try:
90
+ from elasticsearch import AsyncElasticsearch
91
+ from elasticsearch.exceptions import ConnectionError, AuthenticationException
92
+
93
+ # Prepare authentication
94
+ auth_config = {}
95
+
96
+ if self.auth_method == "basic" and self.username and self.password:
97
+ auth_config['basic_auth'] = (self.username, self.password)
98
+ elif self.auth_method == "api_key" and self.api_key_id and self.api_key:
99
+ auth_config['api_key'] = (self.api_key_id, self.api_key)
100
+ elif self.auth_method == "ssl" and self.ssl_fingerprint:
101
+ auth_config['ssl_assert_fingerprint'] = self.ssl_fingerprint
102
+
103
+ # Prepare SSL config
104
+ ssl_config = {
105
+ 'verify_certs': self.verify_certs
106
+ }
107
+ if self.ca_certs:
108
+ ssl_config['ca_certs'] = self.ca_certs
109
+
110
+ # Create client
111
+ client_config = {
112
+ 'hosts': self.hosts,
113
+ 'timeout': self.timeout,
114
+ 'max_retries': self.max_retries,
115
+ **auth_config,
116
+ **ssl_config,
117
+ **self.config
118
+ }
119
+
120
+ self._client = AsyncElasticsearch(**client_config)
121
+
122
+ # Test connection
123
+ try:
124
+ cluster_info = await self._client.info()
125
+ self._cluster_info = {
126
+ 'cluster_name': cluster_info.get('cluster_name'),
127
+ 'version': cluster_info.get('version', {}).get('number'),
128
+ 'lucene_version': cluster_info.get('version', {}).get('lucene_version'),
129
+ 'tagline': cluster_info.get('tagline')
130
+ }
131
+
132
+ logger.info(f"Connected to Elasticsearch cluster '{self._cluster_info['cluster_name']}' "
133
+ f"(version {self._cluster_info['version']})")
134
+
135
+ except AuthenticationException:
136
+ raise RuntimeError("Elasticsearch authentication failed. Please check your credentials.")
137
+ except ConnectionError as e:
138
+ raise RuntimeError(f"Failed to connect to Elasticsearch: {e}")
139
+
140
+ except ImportError:
141
+ raise RuntimeError("elasticsearch not installed. Run: pip install elasticsearch")
142
+ except Exception as e:
143
+ raise RuntimeError(f"Failed to initialize Elasticsearch client: {e}")
144
+
145
+ async def disconnect(self):
146
+ """Close Elasticsearch connection."""
147
+ if self._client:
148
+ await self._client.close()
149
+ self._client = None
150
+ self._cluster_info = None
151
+ logger.info("Disconnected from Elasticsearch")
152
+
153
+ async def search(
154
+ self,
155
+ index: str,
156
+ query: Optional[Dict[str, Any]] = None,
157
+ focus: Optional[List[str]] = None,
158
+ size: int = 100,
159
+ from_: int = 0,
160
+ sort: Optional[List[Dict[str, Any]]] = None,
161
+ aggregations: Optional[Dict[str, Any]] = None,
162
+ scroll: Optional[str] = None,
163
+ **kwargs
164
+ ) -> Dict[str, Any]:
165
+ """
166
+ Search documents in Elasticsearch.
167
+
168
+ Args:
169
+ index: Index name or pattern
170
+ query: Elasticsearch query DSL (defaults to match_all)
171
+ focus: List of fields to return (source filtering)
172
+ size: Number of documents to return
173
+ from_: Starting offset for pagination
174
+ sort: Sort configuration
175
+ aggregations: Aggregations to compute
176
+ scroll: Scroll context keepalive time
177
+ **kwargs: Additional search parameters
178
+
179
+ Returns:
180
+ Search results with hits, aggregations, and metadata
181
+
182
+ Example:
183
+ results = await es.search("logs", {"match": {"level": "ERROR"}}, focus=["timestamp", "message"])
184
+ """
185
+ if self._client is None:
186
+ await self.connect()
187
+
188
+ try:
189
+ # Default to match_all if no query provided
190
+ if query is None:
191
+ query = {"match_all": {}}
192
+
193
+ # Prepare search body
194
+ search_body = {
195
+ "query": query,
196
+ "size": size,
197
+ "from": from_
198
+ }
199
+
200
+ # Apply focus system (source filtering)
201
+ if focus:
202
+ search_body["_source"] = focus
203
+
204
+ # Add sort if specified
205
+ if sort:
206
+ search_body["sort"] = sort
207
+
208
+ # Add aggregations if specified
209
+ if aggregations:
210
+ search_body["aggs"] = aggregations
211
+
212
+ # Prepare search parameters
213
+ search_params = {
214
+ "index": index,
215
+ "body": search_body,
216
+ **kwargs
217
+ }
218
+
219
+ # Add scroll if specified
220
+ if scroll:
221
+ search_params["scroll"] = scroll
222
+
223
+ # Execute search
224
+ response = await self._client.search(**search_params)
225
+
226
+ # Format response
227
+ result = {
228
+ "hits": {
229
+ "total": response["hits"]["total"],
230
+ "max_score": response["hits"].get("max_score"),
231
+ "documents": [hit["_source"] for hit in response["hits"]["hits"]]
232
+ },
233
+ "took": response["took"],
234
+ "timed_out": response["timed_out"],
235
+ "_scroll_id": response.get("_scroll_id")
236
+ }
237
+
238
+ # Add aggregations if present
239
+ if "aggregations" in response:
240
+ result["aggregations"] = response["aggregations"]
241
+
242
+ logger.info(f"Search completed: {result['hits']['total']['value']} hits in {result['took']}ms")
243
+ return result
244
+
245
+ except Exception as e:
246
+ logger.error(f"Elasticsearch search failed: {e}")
247
+ raise RuntimeError(f"Elasticsearch search failed: {e}")
248
+
249
+ async def index_document(
250
+ self,
251
+ index: str,
252
+ document: Dict[str, Any],
253
+ doc_id: Optional[str] = None,
254
+ refresh: Union[bool, str] = False,
255
+ **kwargs
256
+ ) -> Dict[str, Any]:
257
+ """
258
+ Index a single document.
259
+
260
+ Args:
261
+ index: Index name
262
+ document: Document to index
263
+ doc_id: Document ID (auto-generated if not provided)
264
+ refresh: Whether to refresh the index
265
+ **kwargs: Additional indexing parameters
266
+
267
+ Returns:
268
+ Indexing result with document ID and metadata
269
+
270
+ Example:
271
+ result = await es.index_document("logs", {"level": "INFO", "message": "Test"})
272
+ """
273
+ if self._client is None:
274
+ await self.connect()
275
+
276
+ try:
277
+ # Prepare index parameters
278
+ index_params = {
279
+ "index": index,
280
+ "body": document,
281
+ "refresh": refresh,
282
+ **kwargs
283
+ }
284
+
285
+ if doc_id:
286
+ index_params["id"] = doc_id
287
+
288
+ # Index document
289
+ response = await self._client.index(**index_params)
290
+
291
+ result = {
292
+ "id": response["_id"],
293
+ "index": response["_index"],
294
+ "version": response["_version"],
295
+ "result": response["result"],
296
+ "created": response["result"] == "created"
297
+ }
298
+
299
+ logger.debug(f"Indexed document {result['id']} in index {index}")
300
+ return result
301
+
302
+ except Exception as e:
303
+ logger.error(f"Document indexing failed: {e}")
304
+ raise RuntimeError(f"Elasticsearch index_document failed: {e}")
305
+
306
+ async def bulk_index(
307
+ self,
308
+ index: str,
309
+ documents: List[Dict[str, Any]],
310
+ batch_size: int = 1000,
311
+ refresh: Union[bool, str] = False,
312
+ **kwargs
313
+ ) -> Dict[str, Any]:
314
+ """
315
+ Bulk index multiple documents.
316
+
317
+ Args:
318
+ index: Index name
319
+ documents: List of documents to index
320
+ batch_size: Number of documents per batch
321
+ refresh: Whether to refresh the index
322
+ **kwargs: Additional bulk parameters
323
+
324
+ Returns:
325
+ Bulk indexing results with success/error statistics
326
+
327
+ Example:
328
+ result = await es.bulk_index("logs", log_documents, batch_size=5000)
329
+ """
330
+ if self._client is None:
331
+ await self.connect()
332
+
333
+ if not documents:
334
+ return {"indexed": 0, "errors": 0, "took": 0}
335
+
336
+ try:
337
+ from elasticsearch.helpers import async_bulk, BulkIndexError
338
+
339
+ # Prepare documents for bulk indexing
340
+ def doc_generator():
341
+ for doc in documents:
342
+ yield {
343
+ "_index": index,
344
+ "_source": doc,
345
+ **kwargs
346
+ }
347
+
348
+ # Perform bulk indexing
349
+ start_time = datetime.now()
350
+
351
+ try:
352
+ success_count, failed_docs = await async_bulk(
353
+ self._client,
354
+ doc_generator(),
355
+ chunk_size=batch_size,
356
+ refresh=refresh,
357
+ raise_on_error=False,
358
+ raise_on_exception=False
359
+ )
360
+
361
+ end_time = datetime.now()
362
+ duration_ms = (end_time - start_time).total_seconds() * 1000
363
+
364
+ result = {
365
+ "indexed": success_count,
366
+ "errors": len(failed_docs),
367
+ "total": len(documents),
368
+ "took": int(duration_ms),
369
+ "failed_docs": failed_docs if failed_docs else []
370
+ }
371
+
372
+ logger.info(f"Bulk indexed {success_count}/{len(documents)} documents in {duration_ms:.1f}ms")
373
+
374
+ if failed_docs:
375
+ logger.warning(f"Failed to index {len(failed_docs)} documents")
376
+
377
+ return result
378
+
379
+ except BulkIndexError as e:
380
+ logger.error(f"Bulk indexing error: {e}")
381
+ raise RuntimeError(f"Bulk indexing failed: {e}")
382
+
383
+ except Exception as e:
384
+ logger.error(f"Bulk indexing failed: {e}")
385
+ raise RuntimeError(f"Elasticsearch bulk_index failed: {e}")
386
+
387
+ async def delete_document(
388
+ self,
389
+ index: str,
390
+ doc_id: str,
391
+ refresh: Union[bool, str] = False,
392
+ **kwargs
393
+ ) -> Dict[str, Any]:
394
+ """
395
+ Delete a document by ID.
396
+
397
+ Args:
398
+ index: Index name
399
+ doc_id: Document ID
400
+ refresh: Whether to refresh the index
401
+ **kwargs: Additional delete parameters
402
+
403
+ Returns:
404
+ Delete result
405
+
406
+ Example:
407
+ result = await es.delete_document("logs", "doc123")
408
+ """
409
+ if self._client is None:
410
+ await self.connect()
411
+
412
+ try:
413
+ response = await self._client.delete(
414
+ index=index,
415
+ id=doc_id,
416
+ refresh=refresh,
417
+ **kwargs
418
+ )
419
+
420
+ result = {
421
+ "id": response["_id"],
422
+ "index": response["_index"],
423
+ "version": response["_version"],
424
+ "result": response["result"],
425
+ "deleted": response["result"] == "deleted"
426
+ }
427
+
428
+ logger.debug(f"Deleted document {doc_id} from index {index}")
429
+ return result
430
+
431
+ except Exception as e:
432
+ logger.error(f"Document deletion failed: {e}")
433
+ raise RuntimeError(f"Elasticsearch delete_document failed: {e}")
434
+
435
+ async def create_index(
436
+ self,
437
+ index: str,
438
+ mapping: Optional[Dict[str, Any]] = None,
439
+ settings: Optional[Dict[str, Any]] = None,
440
+ **kwargs
441
+ ) -> Dict[str, Any]:
442
+ """
443
+ Create an index with optional mapping and settings.
444
+
445
+ Args:
446
+ index: Index name
447
+ mapping: Index mapping definition
448
+ settings: Index settings
449
+ **kwargs: Additional parameters
450
+
451
+ Returns:
452
+ Index creation result
453
+
454
+ Example:
455
+ result = await es.create_index("logs", mapping={"properties": {"timestamp": {"type": "date"}}})
456
+ """
457
+ if self._client is None:
458
+ await self.connect()
459
+
460
+ try:
461
+ # Prepare index body
462
+ body = {}
463
+ if mapping:
464
+ body["mappings"] = mapping
465
+ if settings:
466
+ body["settings"] = settings
467
+
468
+ # Create index
469
+ response = await self._client.indices.create(
470
+ index=index,
471
+ body=body if body else None,
472
+ **kwargs
473
+ )
474
+
475
+ result = {
476
+ "acknowledged": response["acknowledged"],
477
+ "shards_acknowledged": response.get("shards_acknowledged", False),
478
+ "index": response.get("index", index)
479
+ }
480
+
481
+ logger.info(f"Created index {index}")
482
+ return result
483
+
484
+ except Exception as e:
485
+ logger.error(f"Index creation failed: {e}")
486
+ raise RuntimeError(f"Elasticsearch create_index failed: {e}")
487
+
488
+ async def get_mapping(self, index: str) -> Dict[str, Any]:
489
+ """
490
+ Get index mapping.
491
+
492
+ Args:
493
+ index: Index name
494
+
495
+ Returns:
496
+ Index mapping
497
+
498
+ Example:
499
+ mapping = await es.get_mapping("logs")
500
+ """
501
+ if self._client is None:
502
+ await self.connect()
503
+
504
+ try:
505
+ response = await self._client.indices.get_mapping(index=index)
506
+
507
+ # Extract mapping for the index
508
+ if index in response:
509
+ return response[index].get("mappings", {})
510
+ else:
511
+ # Handle index patterns
512
+ return list(response.values())[0].get("mappings", {}) if response else {}
513
+
514
+ except Exception as e:
515
+ logger.error(f"Get mapping failed: {e}")
516
+ raise RuntimeError(f"Elasticsearch get_mapping failed: {e}")
517
+
518
+ async def search_agent_logs(
519
+ self,
520
+ index: str,
521
+ agent_id: Optional[str] = None,
522
+ status: Optional[str] = None,
523
+ time_range: Optional[Dict[str, str]] = None,
524
+ focus: Optional[List[str]] = None,
525
+ size: int = 100
526
+ ) -> Dict[str, Any]:
527
+ """
528
+ Search agent execution logs with intelligent filtering.
529
+
530
+ Args:
531
+ index: Index name for agent logs
532
+ agent_id: Filter by specific agent ID
533
+ status: Filter by execution status (success, error, etc.)
534
+ time_range: Time range filter {"gte": "now-1h", "lte": "now"}
535
+ focus: Fields to focus on
536
+ size: Number of results to return
537
+
538
+ Returns:
539
+ Filtered agent logs
540
+
541
+ Example:
542
+ logs = await es.search_agent_logs("agent_logs", agent_id="data_processor", status="error")
543
+ """
544
+ # Build query
545
+ must_clauses = []
546
+
547
+ if agent_id:
548
+ must_clauses.append({"term": {"agent_id": agent_id}})
549
+
550
+ if status:
551
+ must_clauses.append({"term": {"status": status}})
552
+
553
+ if time_range:
554
+ must_clauses.append({"range": {"timestamp": time_range}})
555
+
556
+ # Default query if no filters
557
+ if not must_clauses:
558
+ query = {"match_all": {}}
559
+ else:
560
+ query = {"bool": {"must": must_clauses}}
561
+
562
+ # Default focus for agent logs
563
+ if focus is None:
564
+ focus = ["timestamp", "agent_id", "status", "duration_ms", "message", "error"]
565
+
566
+ # Sort by timestamp descending
567
+ sort = [{"timestamp": {"order": "desc"}}]
568
+
569
+ return await self.search(
570
+ index=index,
571
+ query=query,
572
+ focus=focus,
573
+ size=size,
574
+ sort=sort
575
+ )
576
+
577
+ async def index_agent_results(
578
+ self,
579
+ index: str,
580
+ agent_results: Dict[str, Any],
581
+ agent_id: str,
582
+ timestamp: Optional[str] = None
583
+ ) -> Dict[str, Any]:
584
+ """
585
+ Index agent execution results with structured schema.
586
+
587
+ Args:
588
+ index: Index name for agent results
589
+ agent_results: Agent execution results
590
+ agent_id: Agent identifier
591
+ timestamp: Execution timestamp (auto-generated if not provided)
592
+
593
+ Returns:
594
+ Indexing result
595
+
596
+ Example:
597
+ result = await es.index_agent_results("agent_outputs", agent_results, "data_processor")
598
+ """
599
+ # Add metadata to document
600
+ from datetime import timezone
601
+ now = datetime.now(timezone.utc).isoformat()
602
+ document = {
603
+ "agent_id": agent_id,
604
+ "timestamp": timestamp or now,
605
+ "indexed_at": now,
606
+ **agent_results
607
+ }
608
+
609
+ return await self.index_document(index, document)
610
+
611
+ async def analyze_performance(
612
+ self,
613
+ index: str,
614
+ metric_field: str = "duration_ms",
615
+ group_by: Optional[str] = None,
616
+ time_range: Optional[Dict[str, str]] = None,
617
+ focus: Optional[List[str]] = None
618
+ ) -> Dict[str, Any]:
619
+ """
620
+ Analyze agent performance with aggregations.
621
+
622
+ Args:
623
+ index: Index name
624
+ metric_field: Field to analyze (e.g., duration_ms)
625
+ group_by: Field to group by (e.g., agent_id)
626
+ time_range: Time range filter
627
+ focus: Fields to focus on for individual documents
628
+
629
+ Returns:
630
+ Performance analytics with aggregations
631
+
632
+ Example:
633
+ analytics = await es.analyze_performance("agent_metrics", group_by="agent_id")
634
+ """
635
+ # Build query
636
+ query = {"match_all": {}}
637
+ if time_range:
638
+ query = {
639
+ "bool": {
640
+ "must": [{"range": {"timestamp": time_range}}]
641
+ }
642
+ }
643
+
644
+ # Build aggregations
645
+ aggregations = {
646
+ "performance_stats": {
647
+ "stats": {"field": metric_field}
648
+ },
649
+ "performance_percentiles": {
650
+ "percentiles": {"field": metric_field, "percents": [50, 95, 99]}
651
+ }
652
+ }
653
+
654
+ # Group by field if specified
655
+ if group_by:
656
+ aggregations["groups"] = {
657
+ "terms": {"field": group_by},
658
+ "aggs": {
659
+ "group_stats": {"stats": {"field": metric_field}},
660
+ "group_percentiles": {"percentiles": {"field": metric_field, "percents": [50, 95, 99]}}
661
+ }
662
+ }
663
+
664
+ return await self.search(
665
+ index=index,
666
+ query=query,
667
+ focus=focus,
668
+ size=0, # Only return aggregations
669
+ aggregations=aggregations
670
+ )
671
+
672
+ async def get_cluster_health(self) -> Dict[str, Any]:
673
+ """
674
+ Get Elasticsearch cluster health information.
675
+
676
+ Returns:
677
+ Cluster health status and metrics
678
+ """
679
+ if self._client is None:
680
+ await self.connect()
681
+
682
+ try:
683
+ health = await self._client.cluster.health()
684
+
685
+ result = {
686
+ "cluster_name": health["cluster_name"],
687
+ "status": health["status"],
688
+ "timed_out": health["timed_out"],
689
+ "number_of_nodes": health["number_of_nodes"],
690
+ "number_of_data_nodes": health["number_of_data_nodes"],
691
+ "active_primary_shards": health["active_primary_shards"],
692
+ "active_shards": health["active_shards"],
693
+ "relocating_shards": health["relocating_shards"],
694
+ "initializing_shards": health["initializing_shards"],
695
+ "unassigned_shards": health["unassigned_shards"],
696
+ "cluster_info": self._cluster_info
697
+ }
698
+
699
+ return result
700
+
701
+ except Exception as e:
702
+ logger.error(f"Get cluster health failed: {e}")
703
+ raise RuntimeError(f"Elasticsearch get_cluster_health failed: {e}")
704
+
705
+ def get_tools(self) -> List['AgentTool']:
706
+ """
707
+ Expose Elasticsearch operations as agent tools.
708
+
709
+ Returns:
710
+ List of AgentTool instances for Elasticsearch operations
711
+ """
712
+ from ..core.tools import AgentTool
713
+
714
+ return [
715
+ AgentTool(
716
+ name="search_elasticsearch",
717
+ description="Search documents in Elasticsearch index using query DSL. Returns matching documents with scores.",
718
+ parameters={
719
+ "type": "object",
720
+ "properties": {
721
+ "index": {
722
+ "type": "string",
723
+ "description": "Index name or pattern to search in"
724
+ },
725
+ "query": {
726
+ "type": "object",
727
+ "description": "Elasticsearch query DSL object (e.g., {\"match\": {\"field\": \"value\"}})"
728
+ },
729
+ "size": {
730
+ "type": "integer",
731
+ "description": "Number of results to return (default: 100)"
732
+ }
733
+ },
734
+ "required": ["index", "query"]
735
+ },
736
+ handler=self._tool_search,
737
+ category="search",
738
+ source="plugin",
739
+ plugin_name="Elasticsearch",
740
+ timeout_seconds=60
741
+ ),
742
+ AgentTool(
743
+ name="index_document",
744
+ description="Index a single document into Elasticsearch. Creates or updates a document in the specified index.",
745
+ parameters={
746
+ "type": "object",
747
+ "properties": {
748
+ "index": {
749
+ "type": "string",
750
+ "description": "Index name where document will be stored"
751
+ },
752
+ "document": {
753
+ "type": "object",
754
+ "description": "Document data as JSON object"
755
+ },
756
+ "doc_id": {
757
+ "type": "string",
758
+ "description": "Optional document ID (auto-generated if not provided)"
759
+ }
760
+ },
761
+ "required": ["index", "document"]
762
+ },
763
+ handler=self._tool_index,
764
+ category="search",
765
+ source="plugin",
766
+ plugin_name="Elasticsearch",
767
+ timeout_seconds=30
768
+ ),
769
+ AgentTool(
770
+ name="get_index_mapping",
771
+ description="Get the mapping (schema) for an Elasticsearch index to understand its structure",
772
+ parameters={
773
+ "type": "object",
774
+ "properties": {
775
+ "index": {
776
+ "type": "string",
777
+ "description": "Index name to get mapping for"
778
+ }
779
+ },
780
+ "required": ["index"]
781
+ },
782
+ handler=self._tool_get_mapping,
783
+ category="search",
784
+ source="plugin",
785
+ plugin_name="Elasticsearch",
786
+ timeout_seconds=30
787
+ )
788
+ ]
789
+
790
+ async def _tool_search(self, args: Dict[str, Any]) -> Dict[str, Any]:
791
+ """Tool handler for search_elasticsearch"""
792
+ index = args.get("index")
793
+ query = args.get("query")
794
+ size = args.get("size", 100)
795
+
796
+ results = await self.search(index=index, query=query, size=size)
797
+
798
+ return {
799
+ "success": True,
800
+ "documents": results["hits"]["documents"],
801
+ "total": results["hits"]["total"]["value"],
802
+ "max_score": results["hits"]["max_score"],
803
+ "took_ms": results["took"]
804
+ }
805
+
806
+ async def _tool_index(self, args: Dict[str, Any]) -> Dict[str, Any]:
807
+ """Tool handler for index_document"""
808
+ index = args.get("index")
809
+ document = args.get("document")
810
+ doc_id = args.get("doc_id")
811
+
812
+ result = await self.index_document(
813
+ index=index,
814
+ document=document,
815
+ doc_id=doc_id
816
+ )
817
+
818
+ return {
819
+ "success": True,
820
+ "id": result["id"],
821
+ "index": result["index"],
822
+ "created": result["created"],
823
+ "version": result["version"]
824
+ }
825
+
826
+ async def _tool_get_mapping(self, args: Dict[str, Any]) -> Dict[str, Any]:
827
+ """Tool handler for get_index_mapping"""
828
+ index = args.get("index")
829
+
830
+ mapping = await self.get_mapping(index)
831
+
832
+ return {
833
+ "success": True,
834
+ "index": index,
835
+ "mapping": mapping
836
+ }
837
+
838
+ # Context manager support
839
+ async def __aenter__(self):
840
+ await self.connect()
841
+ return self
842
+
843
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
844
+ await self.disconnect()
845
+
846
+
847
+ def elasticsearch(**kwargs) -> ElasticsearchPlugin:
848
+ """Create Elasticsearch plugin with simplified interface."""
849
+ return ElasticsearchPlugin(**kwargs)