agentdeploy 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.
- agentdeploy/__init__.py +32 -0
- agentdeploy/adapters/__init__.py +6 -0
- agentdeploy/adapters/base.py +46 -0
- agentdeploy/adapters/registry.py +221 -0
- agentdeploy/cli/__init__.py +5 -0
- agentdeploy/cli/main.py +266 -0
- agentdeploy/core/__init__.py +1 -0
- agentdeploy/core/app.py +104 -0
- agentdeploy/core/deploy.py +96 -0
- agentdeploy/core/hitl.py +203 -0
- agentdeploy/core/observability.py +160 -0
- agentdeploy/targets/__init__.py +17 -0
- agentdeploy/targets/cloud_run.py +128 -0
- agentdeploy/targets/docker_compose.py +145 -0
- agentdeploy/targets/kubernetes.py +344 -0
- agentdeploy/targets/lambda_target.py +160 -0
- agentdeploy-0.1.1.dist-info/METADATA +337 -0
- agentdeploy-0.1.1.dist-info/RECORD +21 -0
- agentdeploy-0.1.1.dist-info/WHEEL +4 -0
- agentdeploy-0.1.1.dist-info/entry_points.txt +2 -0
- agentdeploy-0.1.1.dist-info/licenses/LICENSE +21 -0
agentdeploy/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentDeploy — zero-boilerplate deployment for AI agent frameworks.
|
|
3
|
+
|
|
4
|
+
Wrap any LangGraph, CrewAI, OpenAI Agents SDK, or custom agent and
|
|
5
|
+
generate production-ready Kubernetes manifests, Docker Compose files,
|
|
6
|
+
Lambda handlers, or Cloud Run services.
|
|
7
|
+
|
|
8
|
+
Public API:
|
|
9
|
+
AgentApp — wrap and configure an agent
|
|
10
|
+
deploy — start a deployment pipeline for an AgentApp
|
|
11
|
+
HITLGate — human-in-the-loop checkpoint primitive
|
|
12
|
+
HITLDecision — APPROVE / REJECT / MODIFY enum
|
|
13
|
+
CheckpointResult — result returned by a HITL checkpoint
|
|
14
|
+
Telemetry — OpenTelemetry-backed agent tracing with cost tracking
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from agentdeploy.core.app import AgentApp
|
|
18
|
+
from agentdeploy.core.deploy import deploy
|
|
19
|
+
from agentdeploy.core.hitl import CheckpointResult, HITLDecision, HITLGate
|
|
20
|
+
from agentdeploy.core.observability import Telemetry
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.1"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AgentApp",
|
|
26
|
+
"deploy",
|
|
27
|
+
"HITLGate",
|
|
28
|
+
"HITLDecision",
|
|
29
|
+
"CheckpointResult",
|
|
30
|
+
"Telemetry",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentAdapter — abstract base class for framework adapters.
|
|
3
|
+
|
|
4
|
+
Each adapter knows how to:
|
|
5
|
+
- validate the wrapped agent object
|
|
6
|
+
- generate the entrypoint server code
|
|
7
|
+
- declare framework-specific dependencies
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentAdapter(ABC):
|
|
17
|
+
"""Base class for all framework adapters."""
|
|
18
|
+
|
|
19
|
+
framework_name: str = "unknown"
|
|
20
|
+
|
|
21
|
+
def __init__(self, agent: Any) -> None:
|
|
22
|
+
self.agent = agent
|
|
23
|
+
self.validate()
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def validate(self) -> None:
|
|
27
|
+
"""Raise ValueError if the wrapped object is not the expected type."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def entrypoint_code(self, app_name: str, port: int) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Return a string of Python code that starts an HTTP server
|
|
33
|
+
exposing POST /invoke and GET /health for this agent.
|
|
34
|
+
This code is written into the container image as server.py.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def pip_extras(self) -> list[str]:
|
|
39
|
+
"""
|
|
40
|
+
Return the pip package names required by this framework
|
|
41
|
+
(e.g. ['langgraph>=0.2', 'langchain-core']).
|
|
42
|
+
These are injected into the generated Dockerfile.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return f"{self.__class__.__name__}(framework={self.framework_name!r})"
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concrete framework adapters and AdapterRegistry.
|
|
3
|
+
|
|
4
|
+
The registry inspects the wrapped object's type and module to
|
|
5
|
+
automatically select the right adapter without the user having
|
|
6
|
+
to specify the framework.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from agentdeploy.adapters.base import AgentAdapter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _LangGraphAdapter(AgentAdapter):
|
|
17
|
+
framework_name = "langgraph"
|
|
18
|
+
|
|
19
|
+
def validate(self) -> None:
|
|
20
|
+
cls_name = type(self.agent).__name__
|
|
21
|
+
if "CompiledGraph" not in cls_name and "CompiledStateGraph" not in cls_name:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"Expected a LangGraph CompiledGraph, got {cls_name}. "
|
|
24
|
+
"Did you forget to call graph.compile()?"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def pip_extras(self) -> list[str]:
|
|
28
|
+
return ["langgraph>=0.2", "langchain-core>=0.2"]
|
|
29
|
+
|
|
30
|
+
def entrypoint_code(self, app_name: str, port: int) -> str:
|
|
31
|
+
return f"""
|
|
32
|
+
import json, os
|
|
33
|
+
from fastapi import FastAPI, Request
|
|
34
|
+
from fastapi.responses import JSONResponse
|
|
35
|
+
import uvicorn
|
|
36
|
+
from agent import graph # imported from user's agent module
|
|
37
|
+
|
|
38
|
+
server = FastAPI(title="{app_name}")
|
|
39
|
+
|
|
40
|
+
@server.get("/health")
|
|
41
|
+
async def health():
|
|
42
|
+
return {{"status": "ok", "framework": "langgraph"}}
|
|
43
|
+
|
|
44
|
+
@server.post("/invoke")
|
|
45
|
+
async def invoke(request: Request):
|
|
46
|
+
body = await request.json()
|
|
47
|
+
result = await graph.ainvoke(body.get("input", {{}}), config=body.get("config", {{}}))
|
|
48
|
+
return JSONResponse(result)
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
uvicorn.run(server, host="0.0.0.0", port={port})
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _CrewAIAdapter(AgentAdapter):
|
|
56
|
+
framework_name = "crewai"
|
|
57
|
+
|
|
58
|
+
def validate(self) -> None:
|
|
59
|
+
cls_name = type(self.agent).__name__
|
|
60
|
+
module = type(self.agent).__module__
|
|
61
|
+
if "crewai" not in module and cls_name != "Crew":
|
|
62
|
+
raise ValueError(f"Expected a CrewAI Crew object, got {cls_name} from {module}.")
|
|
63
|
+
|
|
64
|
+
def pip_extras(self) -> list[str]:
|
|
65
|
+
return ["crewai>=0.70"]
|
|
66
|
+
|
|
67
|
+
def entrypoint_code(self, app_name: str, port: int) -> str:
|
|
68
|
+
return f"""
|
|
69
|
+
import os
|
|
70
|
+
from fastapi import FastAPI, Request
|
|
71
|
+
from fastapi.responses import JSONResponse
|
|
72
|
+
import uvicorn
|
|
73
|
+
from agent import crew # imported from user's agent module
|
|
74
|
+
|
|
75
|
+
server = FastAPI(title="{app_name}")
|
|
76
|
+
|
|
77
|
+
@server.get("/health")
|
|
78
|
+
async def health():
|
|
79
|
+
return {{"status": "ok", "framework": "crewai"}}
|
|
80
|
+
|
|
81
|
+
@server.post("/invoke")
|
|
82
|
+
async def invoke(request: Request):
|
|
83
|
+
body = await request.json()
|
|
84
|
+
result = crew.kickoff(inputs=body.get("inputs", {{}}))
|
|
85
|
+
return JSONResponse({{"result": str(result)}})
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
uvicorn.run(server, host="0.0.0.0", port={port})
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _OpenAIAgentAdapter(AgentAdapter):
|
|
93
|
+
framework_name = "openai-agents"
|
|
94
|
+
|
|
95
|
+
# Modules that legitimately host the OpenAI Agents SDK Agent class.
|
|
96
|
+
_MODULE_PREFIXES = ("agents.", "openai.agents", "openai_agents")
|
|
97
|
+
|
|
98
|
+
def validate(self) -> None:
|
|
99
|
+
module = type(self.agent).__module__
|
|
100
|
+
cls_name = type(self.agent).__name__
|
|
101
|
+
# Be strict: match real module roots, not any module containing
|
|
102
|
+
# the substring "agents" (which would otherwise catch
|
|
103
|
+
# langchain.agents, llama_index.agent, etc.).
|
|
104
|
+
if not (module == "agents" or module.startswith(self._MODULE_PREFIXES)):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Expected an OpenAI Agents SDK Agent, got {cls_name} from "
|
|
107
|
+
f"module {module!r}. Install with `pip install openai-agents` "
|
|
108
|
+
"and pass the Agent instance to .wrap()."
|
|
109
|
+
)
|
|
110
|
+
if cls_name != "Agent":
|
|
111
|
+
raise ValueError(f"Expected an OpenAI Agents SDK Agent class, got {cls_name}.")
|
|
112
|
+
|
|
113
|
+
def pip_extras(self) -> list[str]:
|
|
114
|
+
return ["openai>=1.50", "openai-agents>=0.1"]
|
|
115
|
+
|
|
116
|
+
def entrypoint_code(self, app_name: str, port: int) -> str:
|
|
117
|
+
return f"""
|
|
118
|
+
import os
|
|
119
|
+
from fastapi import FastAPI, Request
|
|
120
|
+
from fastapi.responses import JSONResponse
|
|
121
|
+
from agents import Runner
|
|
122
|
+
import uvicorn
|
|
123
|
+
from agent import agent # imported from user's agent module
|
|
124
|
+
|
|
125
|
+
server = FastAPI(title="{app_name}")
|
|
126
|
+
|
|
127
|
+
@server.get("/health")
|
|
128
|
+
async def health():
|
|
129
|
+
return {{"status": "ok", "framework": "openai-agents"}}
|
|
130
|
+
|
|
131
|
+
@server.post("/invoke")
|
|
132
|
+
async def invoke(request: Request):
|
|
133
|
+
body = await request.json()
|
|
134
|
+
result = await Runner.run(agent, input=body.get("input", ""))
|
|
135
|
+
return JSONResponse({{"result": result.final_output}})
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
uvicorn.run(server, host="0.0.0.0", port={port})
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class _CallableAdapter(AgentAdapter):
|
|
143
|
+
"""Fallback: any async callable is treated as a BYOF agent."""
|
|
144
|
+
|
|
145
|
+
framework_name = "callable"
|
|
146
|
+
|
|
147
|
+
def validate(self) -> None:
|
|
148
|
+
if not callable(self.agent):
|
|
149
|
+
raise ValueError(f"Expected a callable agent, got {type(self.agent).__name__}.")
|
|
150
|
+
|
|
151
|
+
def pip_extras(self) -> list[str]:
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
def entrypoint_code(self, app_name: str, port: int) -> str:
|
|
155
|
+
return f"""
|
|
156
|
+
import os
|
|
157
|
+
from fastapi import FastAPI, Request
|
|
158
|
+
from fastapi.responses import JSONResponse
|
|
159
|
+
import uvicorn
|
|
160
|
+
from agent import agent # your callable
|
|
161
|
+
|
|
162
|
+
server = FastAPI(title="{app_name}")
|
|
163
|
+
|
|
164
|
+
@server.get("/health")
|
|
165
|
+
async def health():
|
|
166
|
+
return {{"status": "ok", "framework": "callable"}}
|
|
167
|
+
|
|
168
|
+
@server.post("/invoke")
|
|
169
|
+
async def invoke(request: Request):
|
|
170
|
+
body = await request.json()
|
|
171
|
+
result = await agent(body.get("input", {{}}))
|
|
172
|
+
return JSONResponse({{"result": result}})
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
uvicorn.run(server, host="0.0.0.0", port={port})
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class AdapterRegistry:
|
|
180
|
+
"""
|
|
181
|
+
Auto-detect the right adapter by inspecting the agent object's
|
|
182
|
+
type name and module path. Ordered from most-specific to most-general.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
_registry: list[tuple[str, type[AgentAdapter]]] = [
|
|
186
|
+
("langgraph", _LangGraphAdapter),
|
|
187
|
+
("crewai", _CrewAIAdapter),
|
|
188
|
+
("openai", _OpenAIAgentAdapter),
|
|
189
|
+
("callable", _CallableAdapter),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def detect(cls, agent: Any) -> AgentAdapter:
|
|
194
|
+
module = type(agent).__module__
|
|
195
|
+
cls_name = type(agent).__name__
|
|
196
|
+
|
|
197
|
+
# LangGraph (most specific)
|
|
198
|
+
if "langgraph" in module or "CompiledGraph" in cls_name or "CompiledStateGraph" in cls_name:
|
|
199
|
+
return _LangGraphAdapter(agent)
|
|
200
|
+
# CrewAI
|
|
201
|
+
if "crewai" in module or cls_name == "Crew":
|
|
202
|
+
return _CrewAIAdapter(agent)
|
|
203
|
+
# OpenAI Agents SDK — strict module-root match to avoid catching
|
|
204
|
+
# langchain.agents / llama_index.agent / etc.
|
|
205
|
+
if (
|
|
206
|
+
module == "agents" or module.startswith(_OpenAIAgentAdapter._MODULE_PREFIXES)
|
|
207
|
+
) and cls_name == "Agent":
|
|
208
|
+
return _OpenAIAgentAdapter(agent)
|
|
209
|
+
# Fallback: any callable
|
|
210
|
+
if callable(agent):
|
|
211
|
+
return _CallableAdapter(agent)
|
|
212
|
+
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Could not detect framework for {cls_name} from {module}. "
|
|
215
|
+
"Wrap it manually: app.wrap(agent, adapter=MyAdapter(agent))"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def register(cls, key: str, adapter_class: type[AgentAdapter]) -> None:
|
|
220
|
+
"""Register a custom adapter for a new framework."""
|
|
221
|
+
cls._registry.insert(0, (key, adapter_class))
|
agentdeploy/cli/main.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentDeploy CLI — agentdeploy <command>
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
init Scaffold a new agentdeploy project
|
|
6
|
+
validate Check an AgentApp config without building
|
|
7
|
+
build Run build() from a config file
|
|
8
|
+
version Print the SDK version
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="agentdeploy",
|
|
24
|
+
help="Zero-boilerplate deployment for AI agent frameworks.",
|
|
25
|
+
add_completion=False,
|
|
26
|
+
)
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command()
|
|
31
|
+
def version():
|
|
32
|
+
"""Print the installed AgentDeploy version."""
|
|
33
|
+
from agentdeploy import __version__
|
|
34
|
+
|
|
35
|
+
console.print(f"[bold]AgentDeploy[/bold] v{__version__}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def init(
|
|
40
|
+
name: str = typer.Argument(..., help="Agent name (used as directory and K8s name)"),
|
|
41
|
+
framework: str = typer.Option(
|
|
42
|
+
"langgraph",
|
|
43
|
+
"--framework",
|
|
44
|
+
"-f",
|
|
45
|
+
help="Agent framework: langgraph | crewai | openai | callable",
|
|
46
|
+
),
|
|
47
|
+
target: str = typer.Option(
|
|
48
|
+
"kubernetes",
|
|
49
|
+
"--target",
|
|
50
|
+
"-t",
|
|
51
|
+
help="Deploy target: kubernetes | docker-compose | lambda | cloud-run",
|
|
52
|
+
),
|
|
53
|
+
):
|
|
54
|
+
"""Scaffold a new AgentDeploy project with example files."""
|
|
55
|
+
proj_dir = Path(name)
|
|
56
|
+
if proj_dir.exists():
|
|
57
|
+
console.print(f"[red]Directory '{name}' already exists.[/red]")
|
|
58
|
+
raise typer.Exit(1)
|
|
59
|
+
|
|
60
|
+
proj_dir.mkdir()
|
|
61
|
+
(proj_dir / "agent.py").write_text(_agent_stub(framework))
|
|
62
|
+
(proj_dir / "agentdeploy_config.py").write_text(_config_stub(name, framework, target))
|
|
63
|
+
(proj_dir / "requirements.txt").write_text(_requirements(framework))
|
|
64
|
+
(proj_dir / ".gitignore").write_text("deploy/\n__pycache__/\n.env\n*.pyc\n")
|
|
65
|
+
(proj_dir / ".env.example").write_text("OPENAI_API_KEY=\nANTHROPIC_API_KEY=\n")
|
|
66
|
+
|
|
67
|
+
console.print(
|
|
68
|
+
Panel(
|
|
69
|
+
f"[bold green]Project '{name}' created.[/bold green]\n\n"
|
|
70
|
+
f" [dim]cd {name}[/dim]\n"
|
|
71
|
+
f" [dim]# Edit agent.py with your {framework} agent[/dim]\n"
|
|
72
|
+
f" [dim]# Edit agentdeploy_config.py to configure deployment[/dim]\n"
|
|
73
|
+
f" [dim]agentdeploy build[/dim]",
|
|
74
|
+
title="agentdeploy init",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command()
|
|
80
|
+
def validate(
|
|
81
|
+
config: str = typer.Option(
|
|
82
|
+
"agentdeploy_config.py",
|
|
83
|
+
"--config",
|
|
84
|
+
"-c",
|
|
85
|
+
help="Path to the agentdeploy config file",
|
|
86
|
+
),
|
|
87
|
+
):
|
|
88
|
+
"""Validate an AgentApp configuration without generating files."""
|
|
89
|
+
cfg_path = Path(config)
|
|
90
|
+
if not cfg_path.exists():
|
|
91
|
+
console.print(f"[red]Config file not found: {config}[/red]")
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
agent_app = _load_app_from_config(cfg_path)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
console.print(f"[red]Config error:[/red] {e}")
|
|
98
|
+
raise typer.Exit(1) from e
|
|
99
|
+
|
|
100
|
+
table = Table(title="AgentApp validation", show_header=False)
|
|
101
|
+
table.add_column("Field", style="dim")
|
|
102
|
+
table.add_column("Value")
|
|
103
|
+
table.add_row("Name", agent_app.name)
|
|
104
|
+
table.add_row("Version", agent_app.version)
|
|
105
|
+
table.add_row(
|
|
106
|
+
"Framework", agent_app._adapter.framework_name if agent_app._adapter else "not wrapped"
|
|
107
|
+
)
|
|
108
|
+
table.add_row("Memory", f"{agent_app._memory_mb} MB")
|
|
109
|
+
table.add_row("Timeout", f"{agent_app._timeout_seconds}s")
|
|
110
|
+
table.add_row("Port", str(agent_app._port))
|
|
111
|
+
table.add_row("Secrets", ", ".join(agent_app._secrets) or "none")
|
|
112
|
+
console.print(table)
|
|
113
|
+
console.print("[green]Validation passed.[/green]")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.command()
|
|
117
|
+
def build(
|
|
118
|
+
config: str = typer.Option(
|
|
119
|
+
"agentdeploy_config.py",
|
|
120
|
+
"--config",
|
|
121
|
+
"-c",
|
|
122
|
+
help="Path to the agentdeploy config file",
|
|
123
|
+
),
|
|
124
|
+
output: str = typer.Option(
|
|
125
|
+
"./deploy",
|
|
126
|
+
"--output",
|
|
127
|
+
"-o",
|
|
128
|
+
help="Output directory for generated artifacts",
|
|
129
|
+
),
|
|
130
|
+
dry_run: bool = typer.Option(
|
|
131
|
+
False,
|
|
132
|
+
"--dry-run",
|
|
133
|
+
help="Validate and show what would be generated without writing files",
|
|
134
|
+
),
|
|
135
|
+
):
|
|
136
|
+
"""Generate deployment artifacts from an agentdeploy config."""
|
|
137
|
+
cfg_path = Path(config)
|
|
138
|
+
if not cfg_path.exists():
|
|
139
|
+
console.print(f"[red]Config file not found: {config}[/red]")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
with console.status("[bold]Loading config...[/bold]"):
|
|
143
|
+
try:
|
|
144
|
+
agent_app = _load_app_from_config(cfg_path)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
console.print(f"[red]Config load error:[/red] {e}")
|
|
147
|
+
raise typer.Exit(1) from e
|
|
148
|
+
|
|
149
|
+
console.print(f"[bold]Building[/bold] {agent_app}")
|
|
150
|
+
|
|
151
|
+
if dry_run:
|
|
152
|
+
console.print("[yellow]Dry run — no files written.[/yellow]")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
from agentdeploy.core.deploy import deploy as _deploy
|
|
157
|
+
|
|
158
|
+
result = _deploy(agent_app).to_kubernetes().with_output_dir(output).build()
|
|
159
|
+
except Exception as e:
|
|
160
|
+
console.print(f"[red]Build failed:[/red] {e}")
|
|
161
|
+
raise typer.Exit(1) from e
|
|
162
|
+
|
|
163
|
+
console.print(
|
|
164
|
+
Panel(
|
|
165
|
+
"\n".join(f" [dim]{f}[/dim]" for f in result.files),
|
|
166
|
+
title=f"[green]Build complete[/green] — {result.output_dir}",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
console.print("\n[bold]Next steps:[/bold]")
|
|
170
|
+
for step in result.next_steps:
|
|
171
|
+
console.print(f" {step}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _load_app_from_config(cfg_path: Path):
|
|
175
|
+
"""
|
|
176
|
+
Import the config module and return the AgentApp it exports.
|
|
177
|
+
Expects the config file to expose `app = AgentApp(...).wrap(...)`.
|
|
178
|
+
"""
|
|
179
|
+
spec = importlib.util.spec_from_file_location("agentdeploy_config", cfg_path)
|
|
180
|
+
module = importlib.util.module_from_spec(spec)
|
|
181
|
+
sys.path.insert(0, str(cfg_path.parent))
|
|
182
|
+
spec.loader.exec_module(module)
|
|
183
|
+
sys.path.pop(0)
|
|
184
|
+
if not hasattr(module, "app"):
|
|
185
|
+
raise AttributeError(
|
|
186
|
+
f"Config file '{cfg_path}' must define a top-level `app = AgentApp(...)` variable."
|
|
187
|
+
)
|
|
188
|
+
return module.app
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _agent_stub(framework: str) -> str:
|
|
192
|
+
stubs = {
|
|
193
|
+
"langgraph": """from langgraph.graph import StateGraph, END
|
|
194
|
+
from typing import TypedDict
|
|
195
|
+
|
|
196
|
+
class State(TypedDict):
|
|
197
|
+
input: str
|
|
198
|
+
output: str
|
|
199
|
+
|
|
200
|
+
def process(state: State) -> State:
|
|
201
|
+
# TODO: add your logic here
|
|
202
|
+
return {"output": f"processed: {state['input']}"}
|
|
203
|
+
|
|
204
|
+
builder = StateGraph(State)
|
|
205
|
+
builder.add_node("process", process)
|
|
206
|
+
builder.set_entry_point("process")
|
|
207
|
+
builder.add_edge("process", END)
|
|
208
|
+
graph = builder.compile()
|
|
209
|
+
""",
|
|
210
|
+
"crewai": """from crewai import Agent, Task, Crew
|
|
211
|
+
|
|
212
|
+
researcher = Agent(
|
|
213
|
+
role="Researcher",
|
|
214
|
+
goal="Find relevant information",
|
|
215
|
+
backstory="Expert researcher with broad knowledge",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
task = Task(
|
|
219
|
+
description="Research the topic: {topic}",
|
|
220
|
+
expected_output="A concise summary",
|
|
221
|
+
agent=researcher,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
crew = Crew(agents=[researcher], tasks=[task])
|
|
225
|
+
""",
|
|
226
|
+
"callable": """async def agent(input: dict) -> dict:
|
|
227
|
+
# TODO: implement your agent logic here
|
|
228
|
+
return {"result": f"processed: {input}"}
|
|
229
|
+
""",
|
|
230
|
+
}
|
|
231
|
+
return stubs.get(framework, stubs["callable"])
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _config_stub(name: str, framework: str, target: str) -> str:
|
|
235
|
+
agent_obj = {"langgraph": "graph", "crewai": "crew", "callable": "agent"}.get(
|
|
236
|
+
framework, "agent"
|
|
237
|
+
)
|
|
238
|
+
return f"""from agentdeploy import AgentApp, deploy
|
|
239
|
+
from agent import {agent_obj}
|
|
240
|
+
|
|
241
|
+
app = (
|
|
242
|
+
AgentApp("{name}", description="My {framework} agent")
|
|
243
|
+
.wrap({agent_obj})
|
|
244
|
+
.env("ANTHROPIC_API_KEY", from_secret="anthropic-key")
|
|
245
|
+
.resources(memory_mb=1024, timeout_seconds=120)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Uncomment to run deployment programmatically:
|
|
249
|
+
# result = deploy(app).to_{target.replace("-", "_")}().build()
|
|
250
|
+
# print(result)
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _requirements(framework: str) -> str:
|
|
255
|
+
base = "agentdeploy\nfastapi\nuvicorn\n"
|
|
256
|
+
extras = {
|
|
257
|
+
"langgraph": "langgraph>=0.2\nlangchain-core>=0.2\n",
|
|
258
|
+
"crewai": "crewai>=0.70\n",
|
|
259
|
+
"openai": "openai>=1.50\n",
|
|
260
|
+
"callable": "",
|
|
261
|
+
}
|
|
262
|
+
return base + extras.get(framework, "")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core primitives: AgentApp, deploy(), HITLGate, Telemetry."""
|
agentdeploy/core/app.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentApp — wraps any agent framework into a deployable unit.
|
|
3
|
+
|
|
4
|
+
Supports LangGraph, CrewAI, OpenAI Agents SDK, Claude SDK,
|
|
5
|
+
and any custom callable agent (BYOF — Bring Your Own Framework).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from agentdeploy.adapters.base import AgentAdapter
|
|
16
|
+
from agentdeploy.adapters.registry import AdapterRegistry
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentConfig(BaseModel):
|
|
20
|
+
name: str
|
|
21
|
+
description: str = ""
|
|
22
|
+
version: str = "0.1.0"
|
|
23
|
+
env_vars: dict[str, str] = {}
|
|
24
|
+
secrets: list[str] = []
|
|
25
|
+
memory_mb: int = 1024
|
|
26
|
+
timeout_seconds: int = 300
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class AgentApp:
|
|
31
|
+
"""
|
|
32
|
+
Entry point for AgentDeploy. Wrap your agent, configure it,
|
|
33
|
+
then call deploy() to generate deployment artifacts.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
app = AgentApp("my-agent", description="Summarisation crew")
|
|
37
|
+
app.wrap(my_langgraph_graph)
|
|
38
|
+
app.env("OPENAI_API_KEY", from_secret="openai-secret")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
description: str = ""
|
|
43
|
+
version: str = "0.1.0"
|
|
44
|
+
_agent: Any = field(default=None, init=False, repr=False)
|
|
45
|
+
_adapter: AgentAdapter | None = field(default=None, init=False, repr=False)
|
|
46
|
+
_env: dict[str, str] = field(default_factory=dict, init=False)
|
|
47
|
+
_secrets: list[str] = field(default_factory=list, init=False)
|
|
48
|
+
_memory_mb: int = field(default=1024, init=False)
|
|
49
|
+
_timeout_seconds: int = field(default=300, init=False)
|
|
50
|
+
_health_path: str = field(default="/health", init=False)
|
|
51
|
+
_port: int = field(default=8080, init=False)
|
|
52
|
+
|
|
53
|
+
def wrap(self, agent: Any) -> AgentApp:
|
|
54
|
+
"""
|
|
55
|
+
Detect the framework of `agent` automatically and wrap it.
|
|
56
|
+
|
|
57
|
+
Supported: LangGraph CompiledGraph, CrewAI Crew,
|
|
58
|
+
OpenAI Agent, Anthropic Claude SDK agent,
|
|
59
|
+
or any async callable (BYOF).
|
|
60
|
+
"""
|
|
61
|
+
self._agent = agent
|
|
62
|
+
self._adapter = AdapterRegistry.detect(agent)
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def env(self, key: str, value: str = "", *, from_secret: str = "") -> AgentApp:
|
|
66
|
+
"""Set an environment variable. Use from_secret for sensitive values."""
|
|
67
|
+
if from_secret:
|
|
68
|
+
self._secrets.append(from_secret)
|
|
69
|
+
self._env[key] = f"secret:{from_secret}"
|
|
70
|
+
else:
|
|
71
|
+
self._env[key] = value
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def resources(self, *, memory_mb: int = 1024, timeout_seconds: int = 300) -> AgentApp:
|
|
75
|
+
"""Set resource limits for the container."""
|
|
76
|
+
self._memory_mb = memory_mb
|
|
77
|
+
self._timeout_seconds = timeout_seconds
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def port(self, port: int) -> AgentApp:
|
|
81
|
+
"""Override the default HTTP port (default: 8080)."""
|
|
82
|
+
self._port = port
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def health_path(self, path: str) -> AgentApp:
|
|
86
|
+
"""Override the default health check path (default: /health)."""
|
|
87
|
+
self._health_path = path
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def to_config(self) -> AgentConfig:
|
|
91
|
+
plain_env = {k: v for k, v in self._env.items() if not v.startswith("secret:")}
|
|
92
|
+
return AgentConfig(
|
|
93
|
+
name=self.name,
|
|
94
|
+
description=self.description,
|
|
95
|
+
version=self.version,
|
|
96
|
+
env_vars=plain_env,
|
|
97
|
+
secrets=self._secrets,
|
|
98
|
+
memory_mb=self._memory_mb,
|
|
99
|
+
timeout_seconds=self._timeout_seconds,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def __repr__(self) -> str:
|
|
103
|
+
framework = self._adapter.framework_name if self._adapter else "unwrapped"
|
|
104
|
+
return f"AgentApp(name={self.name!r}, framework={framework!r}, port={self._port})"
|