traia-iatp 0.1.1__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 traia-iatp might be problematic. Click here for more details.
- traia_iatp/README.md +368 -0
- traia_iatp/__init__.py +30 -0
- traia_iatp/cli/__init__.py +5 -0
- traia_iatp/cli/main.py +483 -0
- traia_iatp/client/__init__.py +10 -0
- traia_iatp/client/a2a_client.py +274 -0
- traia_iatp/client/crewai_a2a_tools.py +335 -0
- traia_iatp/client/grpc_a2a_tools.py +349 -0
- traia_iatp/client/root_path_a2a_client.py +1 -0
- traia_iatp/core/__init__.py +43 -0
- traia_iatp/core/models.py +161 -0
- traia_iatp/mcp/__init__.py +15 -0
- traia_iatp/mcp/client.py +201 -0
- traia_iatp/mcp/mcp_agent_template.py +422 -0
- traia_iatp/mcp/templates/Dockerfile.j2 +56 -0
- traia_iatp/mcp/templates/README.md.j2 +212 -0
- traia_iatp/mcp/templates/cursor-rules.md.j2 +326 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
- traia_iatp/mcp/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/mcp/templates/dockerignore.j2 +47 -0
- traia_iatp/mcp/templates/gitignore.j2 +77 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
- traia_iatp/mcp/templates/pyproject.toml.j2 +26 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +94 -0
- traia_iatp/mcp/templates/server.py.j2 +240 -0
- traia_iatp/mcp/traia_mcp_adapter.py +381 -0
- traia_iatp/preview_diagrams.html +181 -0
- traia_iatp/registry/__init__.py +26 -0
- traia_iatp/registry/atlas_search_indexes.json +280 -0
- traia_iatp/registry/embeddings.py +298 -0
- traia_iatp/registry/iatp_search_api.py +839 -0
- traia_iatp/registry/mongodb_registry.py +771 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
- traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
- traia_iatp/registry/readmes/README.md +251 -0
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
- traia_iatp/server/__init__.py +15 -0
- traia_iatp/server/a2a_server.py +215 -0
- traia_iatp/server/example_template_usage.py +72 -0
- traia_iatp/server/iatp_server_agent_generator.py +237 -0
- traia_iatp/server/iatp_server_template_generator.py +235 -0
- traia_iatp/server/templates/Dockerfile.j2 +49 -0
- traia_iatp/server/templates/README.md +137 -0
- traia_iatp/server/templates/README.md.j2 +425 -0
- traia_iatp/server/templates/__init__.py +1 -0
- traia_iatp/server/templates/__main__.py.j2 +450 -0
- traia_iatp/server/templates/agent.py.j2 +80 -0
- traia_iatp/server/templates/agent_config.json.j2 +22 -0
- traia_iatp/server/templates/agent_executor.py.j2 +264 -0
- traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/server/templates/env.example.j2 +67 -0
- traia_iatp/server/templates/gitignore.j2 +78 -0
- traia_iatp/server/templates/grpc_server.py.j2 +218 -0
- traia_iatp/server/templates/pyproject.toml.j2 +76 -0
- traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
- traia_iatp/server/templates/server.py.j2 +190 -0
- traia_iatp/special_agencies/__init__.py +4 -0
- traia_iatp/special_agencies/registry_search_agency.py +392 -0
- traia_iatp/utils/__init__.py +10 -0
- traia_iatp/utils/docker_utils.py +251 -0
- traia_iatp/utils/general.py +64 -0
- traia_iatp/utils/iatp_utils.py +126 -0
- traia_iatp-0.1.1.dist-info/METADATA +414 -0
- traia_iatp-0.1.1.dist-info/RECORD +72 -0
- traia_iatp-0.1.1.dist-info/WHEEL +5 -0
- traia_iatp-0.1.1.dist-info/entry_points.txt +2 -0
- traia_iatp-0.1.1.dist-info/licenses/LICENSE +21 -0
- traia_iatp-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry Search Utility Agency
|
|
3
|
+
|
|
4
|
+
A special utility agency that provides search capabilities for finding other utility agencies.
|
|
5
|
+
This agency exposes MongoDB registry search functionality via the A2A protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import List, Dict, Any, Optional
|
|
12
|
+
from fastapi import FastAPI, HTTPException
|
|
13
|
+
from a2a import A2AServer, ToolResult
|
|
14
|
+
import uvicorn
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
from ..registry.mongodb_registry import UtilityAgencyRegistry
|
|
18
|
+
from ..utils import get_now_in_utc
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Create FastAPI app
|
|
24
|
+
app = FastAPI(title="Registry Search Utility Agency")
|
|
25
|
+
|
|
26
|
+
# Create A2A server
|
|
27
|
+
a2a_server = A2AServer(
|
|
28
|
+
name="Registry Search Agency",
|
|
29
|
+
description="Searches for utility agencies in the IATP registry",
|
|
30
|
+
version="1.0.0"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# MongoDB registry instance
|
|
34
|
+
_registry: Optional[UtilityAgencyRegistry] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_registry() -> UtilityAgencyRegistry:
|
|
38
|
+
"""Get or create MongoDB registry instance."""
|
|
39
|
+
global _registry
|
|
40
|
+
if _registry is None:
|
|
41
|
+
mongodb_uri = os.getenv("MONGODB_URI")
|
|
42
|
+
if not mongodb_uri:
|
|
43
|
+
raise ValueError("MONGODB_URI environment variable is required")
|
|
44
|
+
_registry = UtilityAgencyRegistry(mongodb_uri)
|
|
45
|
+
return _registry
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@a2a_server.tool(
|
|
49
|
+
name="search_utility_agencies",
|
|
50
|
+
description="Search for utility agencies by query, tags, or capabilities"
|
|
51
|
+
)
|
|
52
|
+
async def search_utility_agencies(
|
|
53
|
+
query: Optional[str] = None,
|
|
54
|
+
tags: Optional[List[str]] = None,
|
|
55
|
+
capabilities: Optional[List[str]] = None,
|
|
56
|
+
limit: int = 10
|
|
57
|
+
) -> ToolResult:
|
|
58
|
+
"""Search for utility agencies in the registry."""
|
|
59
|
+
try:
|
|
60
|
+
registry = get_registry()
|
|
61
|
+
|
|
62
|
+
# Query agencies
|
|
63
|
+
agencies = await registry.query_agencies(
|
|
64
|
+
query=query,
|
|
65
|
+
tags=tags,
|
|
66
|
+
capabilities=capabilities,
|
|
67
|
+
active_only=True,
|
|
68
|
+
limit=limit
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Convert to simple dict format
|
|
72
|
+
results = []
|
|
73
|
+
for agency in agencies:
|
|
74
|
+
results.append({
|
|
75
|
+
"agency_id": agency.agency_id,
|
|
76
|
+
"name": agency.name,
|
|
77
|
+
"description": agency.description,
|
|
78
|
+
"endpoint": str(agency.endpoint),
|
|
79
|
+
"capabilities": agency.capabilities,
|
|
80
|
+
"tags": agency.tags,
|
|
81
|
+
"is_active": agency.is_active
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return ToolResult(
|
|
85
|
+
output=results,
|
|
86
|
+
metadata={
|
|
87
|
+
"count": len(results),
|
|
88
|
+
"query": query,
|
|
89
|
+
"tags": tags,
|
|
90
|
+
"capabilities": capabilities
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Error searching agencies: {e}")
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@a2a_server.tool(
|
|
99
|
+
name="get_agency_by_id",
|
|
100
|
+
description="Get detailed information about a specific utility agency"
|
|
101
|
+
)
|
|
102
|
+
async def get_agency_by_id(agency_id: str) -> ToolResult:
|
|
103
|
+
"""Get a specific agency by ID."""
|
|
104
|
+
try:
|
|
105
|
+
registry = get_registry()
|
|
106
|
+
|
|
107
|
+
agency = await registry.get_agency_by_id(agency_id)
|
|
108
|
+
if not agency:
|
|
109
|
+
return ToolResult(
|
|
110
|
+
output=None,
|
|
111
|
+
metadata={"error": f"Agency {agency_id} not found"}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
result = {
|
|
115
|
+
"agency_id": agency.agency_id,
|
|
116
|
+
"name": agency.name,
|
|
117
|
+
"description": agency.description,
|
|
118
|
+
"endpoint": str(agency.endpoint),
|
|
119
|
+
"capabilities": agency.capabilities,
|
|
120
|
+
"tags": agency.tags,
|
|
121
|
+
"metadata": agency.metadata,
|
|
122
|
+
"registered_at": agency.registered_at.isoformat() if agency.registered_at else None,
|
|
123
|
+
"last_health_check": agency.last_health_check.isoformat() if agency.last_health_check else None,
|
|
124
|
+
"is_active": agency.is_active
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return ToolResult(
|
|
128
|
+
output=result,
|
|
129
|
+
metadata={"agency_id": agency_id}
|
|
130
|
+
)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Error getting agency {agency_id}: {e}")
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@a2a_server.tool(
|
|
137
|
+
name="get_registry_statistics",
|
|
138
|
+
description="Get statistics about the utility agency registry"
|
|
139
|
+
)
|
|
140
|
+
async def get_registry_statistics() -> ToolResult:
|
|
141
|
+
"""Get registry statistics."""
|
|
142
|
+
try:
|
|
143
|
+
registry = get_registry()
|
|
144
|
+
|
|
145
|
+
stats = await registry.get_statistics()
|
|
146
|
+
|
|
147
|
+
return ToolResult(
|
|
148
|
+
output=stats,
|
|
149
|
+
metadata={"timestamp": get_now_in_utc().isoformat()}
|
|
150
|
+
)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.error(f"Error getting statistics: {e}")
|
|
153
|
+
raise
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@a2a_server.tool(
|
|
157
|
+
name="find_agencies_by_capability",
|
|
158
|
+
description="Find all agencies that have a specific capability"
|
|
159
|
+
)
|
|
160
|
+
async def find_agencies_by_capability(capability: str, limit: int = 20) -> ToolResult:
|
|
161
|
+
"""Find agencies by specific capability."""
|
|
162
|
+
try:
|
|
163
|
+
registry = get_registry()
|
|
164
|
+
|
|
165
|
+
agencies = await registry.query_agencies(
|
|
166
|
+
capabilities=[capability],
|
|
167
|
+
active_only=True,
|
|
168
|
+
limit=limit
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
results = []
|
|
172
|
+
for agency in agencies:
|
|
173
|
+
results.append({
|
|
174
|
+
"agency_id": agency.agency_id,
|
|
175
|
+
"name": agency.name,
|
|
176
|
+
"description": agency.description,
|
|
177
|
+
"endpoint": str(agency.endpoint),
|
|
178
|
+
"all_capabilities": agency.capabilities
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return ToolResult(
|
|
182
|
+
output=results,
|
|
183
|
+
metadata={
|
|
184
|
+
"capability": capability,
|
|
185
|
+
"count": len(results)
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Error finding agencies by capability: {e}")
|
|
190
|
+
raise
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# FastAPI routes
|
|
194
|
+
@app.get("/health")
|
|
195
|
+
async def health_check():
|
|
196
|
+
"""Health check endpoint."""
|
|
197
|
+
try:
|
|
198
|
+
# Test MongoDB connection
|
|
199
|
+
registry = get_registry()
|
|
200
|
+
stats = await registry.get_statistics()
|
|
201
|
+
return {
|
|
202
|
+
"status": "healthy",
|
|
203
|
+
"agency": "Registry Search Agency",
|
|
204
|
+
"total_agencies": stats.get("total_agencies", 0),
|
|
205
|
+
"active_agencies": stats.get("active_agencies", 0)
|
|
206
|
+
}
|
|
207
|
+
except Exception as e:
|
|
208
|
+
return {
|
|
209
|
+
"status": "unhealthy",
|
|
210
|
+
"error": str(e)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@app.get("/info")
|
|
215
|
+
async def get_info():
|
|
216
|
+
"""Get agency information."""
|
|
217
|
+
return {
|
|
218
|
+
"name": "Registry Search Agency",
|
|
219
|
+
"description": "Searches for utility agencies in the IATP registry",
|
|
220
|
+
"capabilities": [
|
|
221
|
+
"search_utility_agencies",
|
|
222
|
+
"get_agency_by_id",
|
|
223
|
+
"get_registry_statistics",
|
|
224
|
+
"find_agencies_by_capability"
|
|
225
|
+
],
|
|
226
|
+
"type": "special_agency",
|
|
227
|
+
"version": "1.0.0"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@app.post("/a2a")
|
|
232
|
+
async def a2a_endpoint(request: dict):
|
|
233
|
+
"""Handle A2A protocol requests."""
|
|
234
|
+
try:
|
|
235
|
+
# Process the A2A request
|
|
236
|
+
action = request.get("action")
|
|
237
|
+
parameters = request.get("parameters", {})
|
|
238
|
+
context = request.get("context", {})
|
|
239
|
+
|
|
240
|
+
# Map action to tool
|
|
241
|
+
tool_mapping = {
|
|
242
|
+
"search_utility_agencies": search_utility_agencies,
|
|
243
|
+
"get_agency_by_id": get_agency_by_id,
|
|
244
|
+
"get_registry_statistics": get_registry_statistics,
|
|
245
|
+
"find_agencies_by_capability": find_agencies_by_capability
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if action not in tool_mapping:
|
|
249
|
+
raise HTTPException(status_code=400, detail=f"Unknown action: {action}")
|
|
250
|
+
|
|
251
|
+
# Execute the tool
|
|
252
|
+
tool_func = tool_mapping[action]
|
|
253
|
+
result = await tool_func(**parameters)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"result": result.output,
|
|
257
|
+
"status": "success",
|
|
258
|
+
"metadata": result.metadata
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"A2A request error: {e}")
|
|
263
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@app.on_event("shutdown")
|
|
267
|
+
async def shutdown_event():
|
|
268
|
+
"""Clean up on shutdown."""
|
|
269
|
+
global _registry
|
|
270
|
+
if _registry:
|
|
271
|
+
_registry.close()
|
|
272
|
+
_registry = None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def create_registry_search_agency_structure(output_dir: str = "special_agencies") -> str:
|
|
276
|
+
"""Create the folder structure for the registry search agency."""
|
|
277
|
+
from pathlib import Path
|
|
278
|
+
|
|
279
|
+
agency_path = Path(output_dir) / "registry_search_agency"
|
|
280
|
+
agency_path.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
|
|
282
|
+
# Create Dockerfile
|
|
283
|
+
dockerfile_content = """FROM python:3.12-slim
|
|
284
|
+
|
|
285
|
+
WORKDIR /app
|
|
286
|
+
|
|
287
|
+
# Install system dependencies
|
|
288
|
+
RUN apt-get update && apt-get install -y gcc && rm -rf /var/lib/apt/lists/*
|
|
289
|
+
|
|
290
|
+
# Copy requirements
|
|
291
|
+
COPY requirements.txt .
|
|
292
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
293
|
+
|
|
294
|
+
# Copy agency files
|
|
295
|
+
COPY . .
|
|
296
|
+
|
|
297
|
+
# Expose port
|
|
298
|
+
EXPOSE 8000
|
|
299
|
+
|
|
300
|
+
# Run the server
|
|
301
|
+
CMD ["python", "-m", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
with open(agency_path / "Dockerfile", "w") as f:
|
|
305
|
+
f.write(dockerfile_content)
|
|
306
|
+
|
|
307
|
+
# Create requirements.txt
|
|
308
|
+
requirements = """fastapi>=0.100.0
|
|
309
|
+
uvicorn>=0.20.0
|
|
310
|
+
a2a>=0.1.0
|
|
311
|
+
pymongo>=4.0.0
|
|
312
|
+
python-dotenv>=1.0.0
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
with open(agency_path / "requirements.txt", "w") as f:
|
|
316
|
+
f.write(requirements)
|
|
317
|
+
|
|
318
|
+
# Copy this file as server.py
|
|
319
|
+
import shutil
|
|
320
|
+
shutil.copy(__file__, agency_path / "server.py")
|
|
321
|
+
|
|
322
|
+
# Create README.md
|
|
323
|
+
readme_content = """# Registry Search Utility Agency
|
|
324
|
+
|
|
325
|
+
A special utility agency that provides search capabilities for finding other utility agencies in the IATP registry.
|
|
326
|
+
|
|
327
|
+
## Features
|
|
328
|
+
- Search agencies by text query
|
|
329
|
+
- Filter by tags and capabilities
|
|
330
|
+
- Get detailed agency information
|
|
331
|
+
- View registry statistics
|
|
332
|
+
|
|
333
|
+
## Environment Variables
|
|
334
|
+
- `MONGODB_URI`: MongoDB connection string (required)
|
|
335
|
+
|
|
336
|
+
## Running the Agency
|
|
337
|
+
|
|
338
|
+
### Using Docker
|
|
339
|
+
```bash
|
|
340
|
+
docker build -t registry-search-agency .
|
|
341
|
+
docker run -p 8000:8000 -e MONGODB_URI="your-mongodb-uri" registry-search-agency
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Without Docker
|
|
345
|
+
```bash
|
|
346
|
+
export MONGODB_URI="your-mongodb-uri"
|
|
347
|
+
pip install -r requirements.txt
|
|
348
|
+
python -m uvicorn server:app --host 0.0.0.0 --port 8000
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## A2A Tools Available
|
|
352
|
+
- `search_utility_agencies`: Search for agencies
|
|
353
|
+
- `get_agency_by_id`: Get specific agency details
|
|
354
|
+
- `get_registry_statistics`: Get registry stats
|
|
355
|
+
- `find_agencies_by_capability`: Find agencies by capability
|
|
356
|
+
|
|
357
|
+
## Usage Example
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
from iatp import create_utility_agency_tools
|
|
361
|
+
|
|
362
|
+
# This agency will be discoverable like any other
|
|
363
|
+
tools = create_utility_agency_tools(
|
|
364
|
+
query="registry search"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Use the tool to find other agencies
|
|
368
|
+
result = tools[0]._run(
|
|
369
|
+
action="search_utility_agencies",
|
|
370
|
+
parameters={"query": "data processing", "limit": 5}
|
|
371
|
+
)
|
|
372
|
+
```
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
with open(agency_path / "README.md", "w") as f:
|
|
376
|
+
f.write(readme_content)
|
|
377
|
+
|
|
378
|
+
# Create .env.example
|
|
379
|
+
env_example = """# MongoDB connection string (required)
|
|
380
|
+
MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/iatp
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
with open(agency_path / ".env.example", "w") as f:
|
|
384
|
+
f.write(env_example)
|
|
385
|
+
|
|
386
|
+
logger.info(f"Created registry search agency structure at: {agency_path}")
|
|
387
|
+
return str(agency_path)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
if __name__ == "__main__":
|
|
391
|
+
# Running directly - start the server
|
|
392
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Docker utility functions for running generated utility agents locally."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
import docker
|
|
9
|
+
import httpx
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LocalDockerRunner:
|
|
16
|
+
"""Run generated utility agents locally using their Docker configuration."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize Docker client."""
|
|
20
|
+
try:
|
|
21
|
+
self.client = docker.from_env()
|
|
22
|
+
except Exception as e:
|
|
23
|
+
logger.error(f"Failed to initialize Docker client: {e}")
|
|
24
|
+
raise RuntimeError("Docker is not available. Please ensure Docker is installed and running.")
|
|
25
|
+
|
|
26
|
+
async def run_agent_docker(
|
|
27
|
+
self,
|
|
28
|
+
agent_path: Path,
|
|
29
|
+
port: int = 8000,
|
|
30
|
+
environment: Optional[Dict[str, str]] = None,
|
|
31
|
+
detached: bool = True
|
|
32
|
+
) -> Dict[str, Any]:
|
|
33
|
+
"""Run a generated utility agent using its Docker configuration.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
agent_path: Path to the generated agent directory
|
|
37
|
+
port: Port to expose (default: 8000)
|
|
38
|
+
environment: Additional environment variables
|
|
39
|
+
detached: Whether to run in detached mode
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dict with deployment information
|
|
43
|
+
"""
|
|
44
|
+
agent_path = Path(agent_path)
|
|
45
|
+
|
|
46
|
+
# Verify the agent directory has required files
|
|
47
|
+
required_files = ["Dockerfile", "docker-compose.yml", "pyproject.toml"]
|
|
48
|
+
for file in required_files:
|
|
49
|
+
if not (agent_path / file).exists():
|
|
50
|
+
raise FileNotFoundError(f"Required file {file} not found in {agent_path}")
|
|
51
|
+
|
|
52
|
+
# Create .env file if it doesn't exist
|
|
53
|
+
env_file = agent_path / ".env"
|
|
54
|
+
if not env_file.exists():
|
|
55
|
+
self._create_env_file(agent_path, port, environment)
|
|
56
|
+
|
|
57
|
+
# Use docker-compose to run the agent
|
|
58
|
+
compose_file = agent_path / "docker-compose.yml"
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# Build the image
|
|
62
|
+
logger.info(f"Building Docker image for agent at {agent_path}")
|
|
63
|
+
subprocess.run(
|
|
64
|
+
["docker-compose", "-f", str(compose_file), "build"],
|
|
65
|
+
cwd=str(agent_path),
|
|
66
|
+
check=True,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Run the container
|
|
72
|
+
logger.info(f"Starting container on port {port}")
|
|
73
|
+
cmd = ["docker-compose", "-f", str(compose_file), "up"]
|
|
74
|
+
if detached:
|
|
75
|
+
cmd.append("-d")
|
|
76
|
+
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
cmd,
|
|
79
|
+
cwd=str(agent_path),
|
|
80
|
+
check=True,
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
env={**os.environ, "PORT": str(port)}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Get container info
|
|
87
|
+
container_name = self._get_container_name(agent_path)
|
|
88
|
+
|
|
89
|
+
# Wait for the service to be ready
|
|
90
|
+
if detached:
|
|
91
|
+
await self._wait_for_service(port)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"success": True,
|
|
95
|
+
"container_name": container_name,
|
|
96
|
+
"port": port,
|
|
97
|
+
"base_url": f"http://localhost:{port}",
|
|
98
|
+
"iatp_endpoint": f"http://localhost:{port}/a2a",
|
|
99
|
+
"health_endpoint": f"http://localhost:{port}/health",
|
|
100
|
+
"logs_command": f"docker logs -f {container_name}",
|
|
101
|
+
"stop_command": f"docker-compose -f {compose_file} down"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
except subprocess.CalledProcessError as e:
|
|
105
|
+
logger.error(f"Docker command failed: {e.stderr}")
|
|
106
|
+
raise RuntimeError(f"Failed to run agent: {e.stderr}")
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Error running agent: {e}")
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
async def stop_agent_docker(self, agent_path: Path) -> bool:
|
|
112
|
+
"""Stop a running agent.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
agent_path: Path to the agent directory
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if stopped successfully
|
|
119
|
+
"""
|
|
120
|
+
agent_path = Path(agent_path)
|
|
121
|
+
compose_file = agent_path / "docker-compose.yml"
|
|
122
|
+
|
|
123
|
+
if not compose_file.exists():
|
|
124
|
+
raise FileNotFoundError(f"docker-compose.yml not found in {agent_path}")
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
subprocess.run(
|
|
128
|
+
["docker-compose", "-f", str(compose_file), "down"],
|
|
129
|
+
cwd=str(agent_path),
|
|
130
|
+
check=True,
|
|
131
|
+
capture_output=True,
|
|
132
|
+
text=True
|
|
133
|
+
)
|
|
134
|
+
logger.info(f"Stopped agent at {agent_path}")
|
|
135
|
+
return True
|
|
136
|
+
except subprocess.CalledProcessError as e:
|
|
137
|
+
logger.error(f"Failed to stop agent: {e.stderr}")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def get_agent_logs(self, agent_path: Path, tail: int = 100) -> str:
|
|
141
|
+
"""Get logs from a running agent.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
agent_path: Path to the agent directory
|
|
145
|
+
tail: Number of lines to return
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Log output as string
|
|
149
|
+
"""
|
|
150
|
+
container_name = self._get_container_name(agent_path)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
container = self.client.containers.get(container_name)
|
|
154
|
+
return container.logs(tail=tail).decode('utf-8')
|
|
155
|
+
except docker.errors.NotFound:
|
|
156
|
+
return f"Container {container_name} not found"
|
|
157
|
+
except Exception as e:
|
|
158
|
+
return f"Error getting logs: {e}"
|
|
159
|
+
|
|
160
|
+
def _create_env_file(self, agent_path: Path, port: int, environment: Optional[Dict[str, str]] = None):
|
|
161
|
+
"""Create a .env file for the agent."""
|
|
162
|
+
env_content = [
|
|
163
|
+
f"PORT={port}",
|
|
164
|
+
"HOST=0.0.0.0",
|
|
165
|
+
"LOG_LEVEL=INFO",
|
|
166
|
+
""
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
# Add custom environment variables
|
|
170
|
+
if environment:
|
|
171
|
+
for key, value in environment.items():
|
|
172
|
+
env_content.append(f"{key}={value}")
|
|
173
|
+
|
|
174
|
+
env_file = agent_path / ".env"
|
|
175
|
+
env_file.write_text("\n".join(env_content))
|
|
176
|
+
logger.info(f"Created .env file at {env_file}")
|
|
177
|
+
|
|
178
|
+
def _get_container_name(self, agent_path: Path) -> str:
|
|
179
|
+
"""Get the container name from the agent directory name."""
|
|
180
|
+
return f"{agent_path.name}-local"
|
|
181
|
+
|
|
182
|
+
async def _wait_for_service(self, port: int, timeout: int = 30):
|
|
183
|
+
"""Wait for the service to be ready."""
|
|
184
|
+
start_time = asyncio.get_event_loop().time()
|
|
185
|
+
|
|
186
|
+
async with httpx.AsyncClient() as client:
|
|
187
|
+
while asyncio.get_event_loop().time() - start_time < timeout:
|
|
188
|
+
try:
|
|
189
|
+
response = await client.get(f"http://localhost:{port}/health")
|
|
190
|
+
if response.status_code == 200:
|
|
191
|
+
logger.info(f"Service is ready on port {port}")
|
|
192
|
+
return
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
await asyncio.sleep(1)
|
|
196
|
+
|
|
197
|
+
logger.warning(f"Service did not become ready within {timeout} seconds")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run_generated_agent_locally(
|
|
201
|
+
agent_path: str,
|
|
202
|
+
port: int = 8000,
|
|
203
|
+
environment: Optional[Dict[str, str]] = None
|
|
204
|
+
) -> Dict[str, Any]:
|
|
205
|
+
"""Convenience function to run a generated agent locally.
|
|
206
|
+
|
|
207
|
+
This is a synchronous wrapper around the async functionality.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
agent_path: Path to the generated agent directory
|
|
211
|
+
port: Port to expose
|
|
212
|
+
environment: Additional environment variables
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dict with deployment information
|
|
216
|
+
"""
|
|
217
|
+
runner = LocalDockerRunner()
|
|
218
|
+
return asyncio.run(runner.run_agent_docker(
|
|
219
|
+
Path(agent_path),
|
|
220
|
+
port=port,
|
|
221
|
+
environment=environment
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def use_run_local_docker_script(agent_path: str) -> subprocess.CompletedProcess:
|
|
226
|
+
"""Run the agent using its generated run_local_docker.sh script.
|
|
227
|
+
|
|
228
|
+
This is the simplest way to run a generated agent locally,
|
|
229
|
+
as it uses the script generated from templates.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
agent_path: Path to the generated agent directory
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
CompletedProcess result
|
|
236
|
+
"""
|
|
237
|
+
agent_path = Path(agent_path)
|
|
238
|
+
script_path = agent_path / "run_local_docker.sh"
|
|
239
|
+
|
|
240
|
+
if not script_path.exists():
|
|
241
|
+
raise FileNotFoundError(f"run_local_docker.sh not found in {agent_path}")
|
|
242
|
+
|
|
243
|
+
# Make sure the script is executable
|
|
244
|
+
script_path.chmod(0o755)
|
|
245
|
+
|
|
246
|
+
# Run the script
|
|
247
|
+
return subprocess.run(
|
|
248
|
+
["./run_local_docker.sh"],
|
|
249
|
+
cwd=str(agent_path),
|
|
250
|
+
check=True
|
|
251
|
+
)
|