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.
@@ -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,6 @@
1
+ """Framework adapters and the auto-detection registry."""
2
+
3
+ from agentdeploy.adapters.base import AgentAdapter
4
+ from agentdeploy.adapters.registry import AdapterRegistry
5
+
6
+ __all__ = ["AgentAdapter", "AdapterRegistry"]
@@ -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))
@@ -0,0 +1,5 @@
1
+ """CLI entry point — exposes the `agentdeploy` Typer app."""
2
+
3
+ from agentdeploy.cli.main import app
4
+
5
+ __all__ = ["app"]
@@ -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."""
@@ -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})"