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.

Files changed (72) hide show
  1. traia_iatp/README.md +368 -0
  2. traia_iatp/__init__.py +30 -0
  3. traia_iatp/cli/__init__.py +5 -0
  4. traia_iatp/cli/main.py +483 -0
  5. traia_iatp/client/__init__.py +10 -0
  6. traia_iatp/client/a2a_client.py +274 -0
  7. traia_iatp/client/crewai_a2a_tools.py +335 -0
  8. traia_iatp/client/grpc_a2a_tools.py +349 -0
  9. traia_iatp/client/root_path_a2a_client.py +1 -0
  10. traia_iatp/core/__init__.py +43 -0
  11. traia_iatp/core/models.py +161 -0
  12. traia_iatp/mcp/__init__.py +15 -0
  13. traia_iatp/mcp/client.py +201 -0
  14. traia_iatp/mcp/mcp_agent_template.py +422 -0
  15. traia_iatp/mcp/templates/Dockerfile.j2 +56 -0
  16. traia_iatp/mcp/templates/README.md.j2 +212 -0
  17. traia_iatp/mcp/templates/cursor-rules.md.j2 +326 -0
  18. traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
  19. traia_iatp/mcp/templates/docker-compose.yml.j2 +23 -0
  20. traia_iatp/mcp/templates/dockerignore.j2 +47 -0
  21. traia_iatp/mcp/templates/gitignore.j2 +77 -0
  22. traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
  23. traia_iatp/mcp/templates/pyproject.toml.j2 +26 -0
  24. traia_iatp/mcp/templates/run_local_docker.sh.j2 +94 -0
  25. traia_iatp/mcp/templates/server.py.j2 +240 -0
  26. traia_iatp/mcp/traia_mcp_adapter.py +381 -0
  27. traia_iatp/preview_diagrams.html +181 -0
  28. traia_iatp/registry/__init__.py +26 -0
  29. traia_iatp/registry/atlas_search_indexes.json +280 -0
  30. traia_iatp/registry/embeddings.py +298 -0
  31. traia_iatp/registry/iatp_search_api.py +839 -0
  32. traia_iatp/registry/mongodb_registry.py +771 -0
  33. traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
  34. traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
  35. traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
  36. traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
  37. traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
  38. traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
  39. traia_iatp/registry/readmes/README.md +251 -0
  40. traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
  41. traia_iatp/server/__init__.py +15 -0
  42. traia_iatp/server/a2a_server.py +215 -0
  43. traia_iatp/server/example_template_usage.py +72 -0
  44. traia_iatp/server/iatp_server_agent_generator.py +237 -0
  45. traia_iatp/server/iatp_server_template_generator.py +235 -0
  46. traia_iatp/server/templates/Dockerfile.j2 +49 -0
  47. traia_iatp/server/templates/README.md +137 -0
  48. traia_iatp/server/templates/README.md.j2 +425 -0
  49. traia_iatp/server/templates/__init__.py +1 -0
  50. traia_iatp/server/templates/__main__.py.j2 +450 -0
  51. traia_iatp/server/templates/agent.py.j2 +80 -0
  52. traia_iatp/server/templates/agent_config.json.j2 +22 -0
  53. traia_iatp/server/templates/agent_executor.py.j2 +264 -0
  54. traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
  55. traia_iatp/server/templates/env.example.j2 +67 -0
  56. traia_iatp/server/templates/gitignore.j2 +78 -0
  57. traia_iatp/server/templates/grpc_server.py.j2 +218 -0
  58. traia_iatp/server/templates/pyproject.toml.j2 +76 -0
  59. traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
  60. traia_iatp/server/templates/server.py.j2 +190 -0
  61. traia_iatp/special_agencies/__init__.py +4 -0
  62. traia_iatp/special_agencies/registry_search_agency.py +392 -0
  63. traia_iatp/utils/__init__.py +10 -0
  64. traia_iatp/utils/docker_utils.py +251 -0
  65. traia_iatp/utils/general.py +64 -0
  66. traia_iatp/utils/iatp_utils.py +126 -0
  67. traia_iatp-0.1.1.dist-info/METADATA +414 -0
  68. traia_iatp-0.1.1.dist-info/RECORD +72 -0
  69. traia_iatp-0.1.1.dist-info/WHEEL +5 -0
  70. traia_iatp-0.1.1.dist-info/entry_points.txt +2 -0
  71. traia_iatp-0.1.1.dist-info/licenses/LICENSE +21 -0
  72. 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,10 @@
1
+ """Traia IATP utility functions module."""
2
+
3
+ from .general import get_now_in_utc, get_logger, is_empty, not_empty
4
+
5
+ __all__ = [
6
+ "get_now_in_utc",
7
+ "get_logger",
8
+ "is_empty",
9
+ "not_empty",
10
+ ]
@@ -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
+ )