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.
- daita/__init__.py +216 -0
- daita/agents/__init__.py +33 -0
- daita/agents/base.py +743 -0
- daita/agents/substrate.py +1141 -0
- daita/cli/__init__.py +145 -0
- daita/cli/__main__.py +7 -0
- daita/cli/ascii_art.py +44 -0
- daita/cli/core/__init__.py +0 -0
- daita/cli/core/create.py +254 -0
- daita/cli/core/deploy.py +473 -0
- daita/cli/core/deployments.py +309 -0
- daita/cli/core/import_detector.py +219 -0
- daita/cli/core/init.py +481 -0
- daita/cli/core/logs.py +239 -0
- daita/cli/core/managed_deploy.py +709 -0
- daita/cli/core/run.py +648 -0
- daita/cli/core/status.py +421 -0
- daita/cli/core/test.py +239 -0
- daita/cli/core/webhooks.py +172 -0
- daita/cli/main.py +588 -0
- daita/cli/utils.py +541 -0
- daita/config/__init__.py +62 -0
- daita/config/base.py +159 -0
- daita/config/settings.py +184 -0
- daita/core/__init__.py +262 -0
- daita/core/decision_tracing.py +701 -0
- daita/core/exceptions.py +480 -0
- daita/core/focus.py +251 -0
- daita/core/interfaces.py +76 -0
- daita/core/plugin_tracing.py +550 -0
- daita/core/relay.py +779 -0
- daita/core/reliability.py +381 -0
- daita/core/scaling.py +459 -0
- daita/core/tools.py +554 -0
- daita/core/tracing.py +770 -0
- daita/core/workflow.py +1144 -0
- daita/display/__init__.py +1 -0
- daita/display/console.py +160 -0
- daita/execution/__init__.py +58 -0
- daita/execution/client.py +856 -0
- daita/execution/exceptions.py +92 -0
- daita/execution/models.py +317 -0
- daita/llm/__init__.py +60 -0
- daita/llm/anthropic.py +291 -0
- daita/llm/base.py +530 -0
- daita/llm/factory.py +101 -0
- daita/llm/gemini.py +355 -0
- daita/llm/grok.py +219 -0
- daita/llm/mock.py +172 -0
- daita/llm/openai.py +220 -0
- daita/plugins/__init__.py +141 -0
- daita/plugins/base.py +37 -0
- daita/plugins/base_db.py +167 -0
- daita/plugins/elasticsearch.py +849 -0
- daita/plugins/mcp.py +481 -0
- daita/plugins/mongodb.py +520 -0
- daita/plugins/mysql.py +362 -0
- daita/plugins/postgresql.py +342 -0
- daita/plugins/redis_messaging.py +500 -0
- daita/plugins/rest.py +537 -0
- daita/plugins/s3.py +770 -0
- daita/plugins/slack.py +729 -0
- daita/utils/__init__.py +18 -0
- daita_agents-0.2.0.dist-info/METADATA +409 -0
- daita_agents-0.2.0.dist-info/RECORD +69 -0
- daita_agents-0.2.0.dist-info/WHEEL +5 -0
- daita_agents-0.2.0.dist-info/entry_points.txt +2 -0
- daita_agents-0.2.0.dist-info/licenses/LICENSE +56 -0
- 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)
|