substrai-agentdeploy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ """
2
+ AgentDeploy - Zero-to-Production AI Agent Deployment Framework
3
+
4
+ Takes any agent definition (LangChain, CrewAI, custom) and deploys it
5
+ as a production-grade serverless API with auth, scaling, monitoring,
6
+ cost controls, multi-tenancy, and session management.
7
+
8
+ Usage:
9
+ from agentdeploy import agent, Tool, Session
10
+
11
+ @agent(name="my-agent", model="bedrock/claude-3-sonnet")
12
+ def my_agent(message: str, session: Session) -> str:
13
+ return "Hello!"
14
+ """
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ from agentdeploy.core.agent import agent, AgentConfig
19
+ from agentdeploy.core.runtime import AgentRuntime, InvocationResult
20
+ from agentdeploy.session.manager import Session, SessionManager
21
+ from agentdeploy.tools.registry import Tool, ToolRegistry, ToolPermission
22
+ from agentdeploy.adapters.base import BaseAdapter
23
+ from agentdeploy.cost.enforcer import CostEnforcer, CostBudget
24
+
25
+ __all__ = [
26
+ "agent",
27
+ "AgentConfig",
28
+ "AgentRuntime",
29
+ "InvocationResult",
30
+ "Session",
31
+ "SessionManager",
32
+ "Tool",
33
+ "ToolRegistry",
34
+ "ToolPermission",
35
+ "BaseAdapter",
36
+ "CostEnforcer",
37
+ "CostBudget",
38
+ ]
File without changes
@@ -0,0 +1,60 @@
1
+ """Base adapter interface for agent frameworks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Dict, Optional
7
+
8
+ from agentdeploy.session.manager import Session
9
+
10
+
11
+ class BaseAdapter(ABC):
12
+ """Base class for agent framework adapters.
13
+
14
+ Implement this to support any agent framework (LangChain, CrewAI, etc.)
15
+ """
16
+
17
+ name: str = "base"
18
+
19
+ @abstractmethod
20
+ def invoke(self, message: str, session: Session, **kwargs) -> str:
21
+ """Invoke the agent with a message.
22
+
23
+ Args:
24
+ message: User message
25
+ session: Current session with history
26
+
27
+ Returns:
28
+ Agent response string
29
+ """
30
+ pass
31
+
32
+ def get_tool_calls(self) -> list:
33
+ """Get tool calls made during last invocation."""
34
+ return []
35
+
36
+ def get_token_usage(self) -> Dict[str, int]:
37
+ """Get token usage from last invocation."""
38
+ return {"input_tokens": 0, "output_tokens": 0}
39
+
40
+
41
+ class CustomAdapter(BaseAdapter):
42
+ """Adapter for custom agent functions (default)."""
43
+
44
+ name = "custom"
45
+
46
+ def __init__(self, agent_fn):
47
+ self.agent_fn = agent_fn
48
+
49
+ def invoke(self, message: str, session: Session, **kwargs) -> str:
50
+ result = self.agent_fn(message, session)
51
+ return str(result) if result else ""
52
+
53
+
54
+ class EchoAdapter(BaseAdapter):
55
+ """Simple echo adapter for testing."""
56
+
57
+ name = "echo"
58
+
59
+ def invoke(self, message: str, session: Session, **kwargs) -> str:
60
+ return f"Echo: {message}"
File without changes
File without changes
@@ -0,0 +1,109 @@
1
+ """Agent decorator and configuration.
2
+
3
+ The @agent decorator turns a Python function into a deployable agent
4
+ with session management, tool access, and cost tracking.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import functools
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+
14
+ @dataclass
15
+ class AgentConfig:
16
+ """Configuration for a deployed agent."""
17
+
18
+ name: str
19
+ model: str = "bedrock/claude-3-sonnet"
20
+ system_prompt: str = "You are a helpful assistant."
21
+ tools: List[Any] = field(default_factory=list)
22
+ max_iterations: int = 10
23
+ timeout_seconds: int = 60
24
+ memory_strategy: str = "sliding_window"
25
+ memory_window_size: int = 20
26
+ memory_ttl_hours: int = 24
27
+ metadata: Dict[str, Any] = field(default_factory=dict)
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ return {
31
+ "name": self.name,
32
+ "model": self.model,
33
+ "system_prompt": self.system_prompt[:50] + "..." if len(self.system_prompt) > 50 else self.system_prompt,
34
+ "tools": [getattr(t, "name", str(t)) for t in self.tools],
35
+ "max_iterations": self.max_iterations,
36
+ "timeout_seconds": self.timeout_seconds,
37
+ "memory_strategy": self.memory_strategy,
38
+ }
39
+
40
+
41
+ class AgentFunction:
42
+ """Wrapper around a decorated agent function."""
43
+
44
+ def __init__(self, fn: Callable, config: AgentConfig):
45
+ self.fn = fn
46
+ self.config = config
47
+ self.name = config.name
48
+ functools.update_wrapper(self, fn)
49
+
50
+ def __call__(self, *args, **kwargs):
51
+ return self.fn(*args, **kwargs)
52
+
53
+ def invoke(self, message: str, session_id: Optional[str] = None) -> Dict[str, Any]:
54
+ """Invoke the agent with a message.
55
+
56
+ Args:
57
+ message: User message
58
+ session_id: Optional session ID for conversation continuity
59
+
60
+ Returns:
61
+ Dict with response and metadata
62
+ """
63
+ from agentdeploy.core.runtime import AgentRuntime
64
+ runtime = AgentRuntime(self.config)
65
+ return runtime.invoke(message, session_id=session_id, agent_fn=self.fn)
66
+
67
+ def __repr__(self) -> str:
68
+ return f"AgentFunction(name='{self.name}', model='{self.config.model}')"
69
+
70
+
71
+ def agent(
72
+ name: str,
73
+ model: str = "bedrock/claude-3-sonnet",
74
+ system_prompt: str = "You are a helpful assistant.",
75
+ tools: Optional[List[Any]] = None,
76
+ max_iterations: int = 10,
77
+ timeout_seconds: int = 60,
78
+ memory_strategy: str = "sliding_window",
79
+ **kwargs,
80
+ ) -> Callable:
81
+ """Decorator to turn a function into a deployable agent.
82
+
83
+ Usage:
84
+ @agent(name="my-agent", model="bedrock/claude-3-sonnet", tools=[search_kb])
85
+ def my_agent(message: str, session: Session) -> str:
86
+ pass
87
+
88
+ Args:
89
+ name: Agent name (used for deployment and routing)
90
+ model: LLM model identifier
91
+ system_prompt: System prompt for the agent
92
+ tools: List of Tool-decorated functions
93
+ max_iterations: Max reasoning loops
94
+ timeout_seconds: Max execution time
95
+ memory_strategy: Session memory strategy
96
+ """
97
+ def decorator(fn: Callable) -> AgentFunction:
98
+ config = AgentConfig(
99
+ name=name,
100
+ model=model,
101
+ system_prompt=system_prompt,
102
+ tools=tools or [],
103
+ max_iterations=max_iterations,
104
+ timeout_seconds=timeout_seconds,
105
+ memory_strategy=memory_strategy,
106
+ metadata=kwargs,
107
+ )
108
+ return AgentFunction(fn, config)
109
+ return decorator
@@ -0,0 +1,191 @@
1
+ """Agent runtime - handles the request lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Callable, Dict, List, Optional
8
+
9
+ from agentdeploy.core.agent import AgentConfig
10
+ from agentdeploy.session.manager import Session, SessionManager
11
+ from agentdeploy.tools.registry import ToolRegistry, ToolCallResult
12
+ from agentdeploy.cost.enforcer import CostEnforcer, CostBudget
13
+
14
+
15
+ @dataclass
16
+ class InvocationResult:
17
+ """Result of an agent invocation."""
18
+
19
+ response: str
20
+ session_id: str
21
+ success: bool = True
22
+ latency_ms: float = 0.0
23
+ input_tokens: int = 0
24
+ output_tokens: int = 0
25
+ cost: float = 0.0
26
+ tool_calls: List[Dict[str, Any]] = field(default_factory=list)
27
+ turn_count: int = 0
28
+ error: Optional[str] = None
29
+ metadata: Dict[str, Any] = field(default_factory=dict)
30
+
31
+ def to_dict(self) -> Dict[str, Any]:
32
+ return {
33
+ "response": self.response,
34
+ "session_id": self.session_id,
35
+ "success": self.success,
36
+ "latency_ms": self.latency_ms,
37
+ "input_tokens": self.input_tokens,
38
+ "output_tokens": self.output_tokens,
39
+ "cost": self.cost,
40
+ "tool_calls": self.tool_calls,
41
+ "turn_count": self.turn_count,
42
+ "error": self.error,
43
+ }
44
+
45
+
46
+ class AgentRuntime:
47
+ """Handles the full agent request lifecycle.
48
+
49
+ Request flow:
50
+ 1. Load/create session
51
+ 2. Check cost budget
52
+ 3. Execute agent function
53
+ 4. Track tool calls
54
+ 5. Record cost
55
+ 6. Save session
56
+ 7. Return result with metadata
57
+
58
+ Usage:
59
+ runtime = AgentRuntime(config)
60
+ result = runtime.invoke("Hello!", session_id="sess-123")
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ config: AgentConfig,
66
+ session_manager: Optional[SessionManager] = None,
67
+ tool_registry: Optional[ToolRegistry] = None,
68
+ cost_enforcer: Optional[CostEnforcer] = None,
69
+ ):
70
+ self.config = config
71
+ self.session_manager = session_manager or SessionManager(ttl_hours=config.memory_ttl_hours)
72
+ self.tool_registry = tool_registry or ToolRegistry()
73
+ self.cost_enforcer = cost_enforcer or CostEnforcer()
74
+ self._invocation_log: List[InvocationResult] = []
75
+
76
+ # Register tools
77
+ for tool in config.tools:
78
+ if hasattr(tool, "definition"):
79
+ self.tool_registry.register(tool)
80
+
81
+ def invoke(
82
+ self,
83
+ message: str,
84
+ session_id: Optional[str] = None,
85
+ tenant_id: str = "default",
86
+ agent_fn: Optional[Callable] = None,
87
+ ) -> InvocationResult:
88
+ """Invoke the agent with full lifecycle management.
89
+
90
+ Args:
91
+ message: User message
92
+ session_id: Optional session ID
93
+ tenant_id: Tenant making the request
94
+ agent_fn: Agent function to call
95
+
96
+ Returns:
97
+ InvocationResult with response and metadata
98
+ """
99
+ start_time = time.time()
100
+
101
+ # 1. Load/create session
102
+ session = self.session_manager.get_or_create(
103
+ session_id, self.config.name, tenant_id
104
+ )
105
+
106
+ # 2. Check cost budget
107
+ cost_check = self.cost_enforcer.check_request(
108
+ session_id=session.session_id, estimated_cost=0.01
109
+ )
110
+ if not cost_check.allowed:
111
+ return InvocationResult(
112
+ response="",
113
+ session_id=session.session_id,
114
+ success=False,
115
+ error=cost_check.message,
116
+ latency_ms=(time.time() - start_time) * 1000,
117
+ )
118
+
119
+ # 3. Add user message to session
120
+ session.add_message("user", message)
121
+
122
+ # 4. Execute agent
123
+ try:
124
+ if agent_fn:
125
+ response = agent_fn(message, session)
126
+ if response is None:
127
+ # Default behavior: echo with context
128
+ response = f"[{self.config.name}] Received: {message}"
129
+ else:
130
+ response = f"[{self.config.name}] Received: {message}"
131
+ except Exception as e:
132
+ return InvocationResult(
133
+ response="",
134
+ session_id=session.session_id,
135
+ success=False,
136
+ error=str(e),
137
+ latency_ms=(time.time() - start_time) * 1000,
138
+ )
139
+
140
+ # 5. Add assistant response to session
141
+ session.add_message("assistant", response)
142
+
143
+ # 6. Calculate cost (estimate)
144
+ input_tokens = len(message) // 4
145
+ output_tokens = len(response) // 4
146
+ cost = self._estimate_cost(input_tokens, output_tokens)
147
+
148
+ # 7. Record cost
149
+ session.total_tokens += input_tokens + output_tokens
150
+ session.total_cost += cost
151
+ self.cost_enforcer.record_cost(session.session_id, cost, input_tokens + output_tokens)
152
+
153
+ # 8. Save session
154
+ self.session_manager.save(session)
155
+
156
+ latency_ms = (time.time() - start_time) * 1000
157
+
158
+ result = InvocationResult(
159
+ response=response,
160
+ session_id=session.session_id,
161
+ success=True,
162
+ latency_ms=round(latency_ms, 2),
163
+ input_tokens=input_tokens,
164
+ output_tokens=output_tokens,
165
+ cost=round(cost, 8),
166
+ tool_calls=[r.__dict__ for r in self.tool_registry.call_log[-5:]],
167
+ turn_count=session.turn_count,
168
+ )
169
+
170
+ self._invocation_log.append(result)
171
+ return result
172
+
173
+ def get_session(self, session_id: str) -> Optional[Session]:
174
+ """Get a session by ID."""
175
+ return self.session_manager.get(session_id)
176
+
177
+ @property
178
+ def invocation_history(self) -> List[InvocationResult]:
179
+ return self._invocation_log
180
+
181
+ def _estimate_cost(self, input_tokens: int, output_tokens: int) -> float:
182
+ """Estimate cost based on model."""
183
+ pricing = {
184
+ "bedrock/claude-3-haiku": (0.00025, 0.00125),
185
+ "bedrock/claude-3-sonnet": (0.003, 0.015),
186
+ "bedrock/claude-3-opus": (0.015, 0.075),
187
+ "openai/gpt-4o-mini": (0.00015, 0.0006),
188
+ "openai/gpt-4o": (0.005, 0.015),
189
+ }
190
+ input_price, output_price = pricing.get(self.config.model, (0.001, 0.002))
191
+ return (input_tokens / 1000) * input_price + (output_tokens / 1000) * output_price
File without changes
@@ -0,0 +1,173 @@
1
+ """Cost enforcement - circuit breakers and budget tracking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ class BudgetAction(Enum):
12
+ ALLOW = "allow"
13
+ BLOCK = "block"
14
+ DOWNGRADE = "downgrade"
15
+ ALERT = "alert"
16
+ KILL = "kill"
17
+
18
+
19
+ @dataclass
20
+ class CostBudget:
21
+ """Budget configuration for cost enforcement."""
22
+
23
+ max_cost_per_request: float = 0.50
24
+ max_cost_per_session: float = 5.00
25
+ daily_budget: float = 50.00
26
+ monthly_budget: float = 1000.00
27
+ on_exceed: str = "block" # block | downgrade | alert | kill
28
+
29
+ def get_action(self) -> BudgetAction:
30
+ return BudgetAction(self.on_exceed)
31
+
32
+
33
+ @dataclass
34
+ class CostRecord:
35
+ """Tracks cost for a specific scope."""
36
+
37
+ scope: str # "request", "session", "tenant", "global"
38
+ scope_id: str
39
+ total_cost: float = 0.0
40
+ request_count: int = 0
41
+ period_start: float = field(default_factory=time.time)
42
+
43
+
44
+ @dataclass
45
+ class CostCheckResult:
46
+ """Result of a cost enforcement check."""
47
+
48
+ allowed: bool
49
+ action: BudgetAction
50
+ current_cost: float
51
+ budget_limit: float
52
+ usage_percent: float
53
+ message: str
54
+ suggested_model: Optional[str] = None # for downgrade action
55
+
56
+
57
+ class CostEnforcer:
58
+ """Enforces cost budgets and circuit breakers.
59
+
60
+ Usage:
61
+ enforcer = CostEnforcer(budget)
62
+ check = enforcer.check_request(session_id="sess-1", estimated_cost=0.05)
63
+ if not check.allowed:
64
+ return error_response(check.message)
65
+ # ... execute agent ...
66
+ enforcer.record_cost(session_id="sess-1", cost=0.03)
67
+ """
68
+
69
+ def __init__(self, budget: Optional[CostBudget] = None):
70
+ self.budget = budget or CostBudget()
71
+ self._session_costs: Dict[str, CostRecord] = {}
72
+ self._daily_cost: CostRecord = CostRecord(scope="daily", scope_id="global")
73
+ self._request_log: List[Dict[str, Any]] = []
74
+
75
+ def check_request(
76
+ self, session_id: str, estimated_cost: float = 0.0, tenant_id: str = "default"
77
+ ) -> CostCheckResult:
78
+ """Check if a request is within budget.
79
+
80
+ Args:
81
+ session_id: Current session ID
82
+ estimated_cost: Estimated cost of this request
83
+ tenant_id: Tenant making the request
84
+
85
+ Returns:
86
+ CostCheckResult with allow/deny decision
87
+ """
88
+ # Check session budget
89
+ session_record = self._session_costs.get(session_id)
90
+ if session_record:
91
+ projected = session_record.total_cost + estimated_cost
92
+ if projected > self.budget.max_cost_per_session:
93
+ return CostCheckResult(
94
+ allowed=False,
95
+ action=self.budget.get_action(),
96
+ current_cost=session_record.total_cost,
97
+ budget_limit=self.budget.max_cost_per_session,
98
+ usage_percent=session_record.total_cost / self.budget.max_cost_per_session,
99
+ message=f"Session budget exceeded: ${session_record.total_cost:.4f} >= ${self.budget.max_cost_per_session}",
100
+ suggested_model="bedrock/claude-3-haiku" if self.budget.on_exceed == "downgrade" else None,
101
+ )
102
+
103
+ # Check per-request budget
104
+ if estimated_cost > self.budget.max_cost_per_request:
105
+ return CostCheckResult(
106
+ allowed=False,
107
+ action=BudgetAction.BLOCK,
108
+ current_cost=estimated_cost,
109
+ budget_limit=self.budget.max_cost_per_request,
110
+ usage_percent=estimated_cost / self.budget.max_cost_per_request,
111
+ message=f"Request cost ${estimated_cost:.4f} exceeds per-request limit ${self.budget.max_cost_per_request}",
112
+ )
113
+
114
+ # Check daily budget
115
+ daily_projected = self._daily_cost.total_cost + estimated_cost
116
+ if daily_projected > self.budget.daily_budget:
117
+ return CostCheckResult(
118
+ allowed=False,
119
+ action=self.budget.get_action(),
120
+ current_cost=self._daily_cost.total_cost,
121
+ budget_limit=self.budget.daily_budget,
122
+ usage_percent=self._daily_cost.total_cost / self.budget.daily_budget,
123
+ message=f"Daily budget exceeded: ${self._daily_cost.total_cost:.2f} >= ${self.budget.daily_budget}",
124
+ suggested_model="bedrock/claude-3-haiku" if self.budget.on_exceed == "downgrade" else None,
125
+ )
126
+
127
+ # Allowed
128
+ usage_pct = self._daily_cost.total_cost / self.budget.daily_budget if self.budget.daily_budget > 0 else 0
129
+ return CostCheckResult(
130
+ allowed=True,
131
+ action=BudgetAction.ALLOW,
132
+ current_cost=self._daily_cost.total_cost,
133
+ budget_limit=self.budget.daily_budget,
134
+ usage_percent=usage_pct,
135
+ message="Within budget",
136
+ )
137
+
138
+ def record_cost(self, session_id: str, cost: float, tokens: int = 0) -> None:
139
+ """Record actual cost after execution."""
140
+ # Session cost
141
+ if session_id not in self._session_costs:
142
+ self._session_costs[session_id] = CostRecord(scope="session", scope_id=session_id)
143
+ self._session_costs[session_id].total_cost += cost
144
+ self._session_costs[session_id].request_count += 1
145
+
146
+ # Daily cost
147
+ self._daily_cost.total_cost += cost
148
+ self._daily_cost.request_count += 1
149
+
150
+ # Log
151
+ self._request_log.append({
152
+ "session_id": session_id,
153
+ "cost": cost,
154
+ "tokens": tokens,
155
+ "timestamp": time.time(),
156
+ })
157
+
158
+ def get_session_cost(self, session_id: str) -> float:
159
+ """Get total cost for a session."""
160
+ record = self._session_costs.get(session_id)
161
+ return record.total_cost if record else 0.0
162
+
163
+ def get_daily_cost(self) -> float:
164
+ """Get total daily cost."""
165
+ return self._daily_cost.total_cost
166
+
167
+ def get_daily_remaining(self) -> float:
168
+ """Get remaining daily budget."""
169
+ return max(self.budget.daily_budget - self._daily_cost.total_cost, 0)
170
+
171
+ def reset_daily(self) -> None:
172
+ """Reset daily cost counter."""
173
+ self._daily_cost = CostRecord(scope="daily", scope_id="global")
File without changes
File without changes
@@ -0,0 +1,154 @@
1
+ """Session management - conversation persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ import uuid
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ @dataclass
12
+ class Message:
13
+ """A single message in a conversation."""
14
+
15
+ role: str # "user", "assistant", "system", "tool"
16
+ content: str
17
+ timestamp: float = field(default_factory=time.time)
18
+ metadata: Dict[str, Any] = field(default_factory=dict)
19
+ tool_call_id: Optional[str] = None
20
+
21
+ def to_dict(self) -> Dict[str, Any]:
22
+ return {"role": self.role, "content": self.content, "timestamp": self.timestamp}
23
+
24
+
25
+ @dataclass
26
+ class Session:
27
+ """Agent conversation session with memory."""
28
+
29
+ session_id: str
30
+ agent_name: str
31
+ tenant_id: str = "default"
32
+ messages: List[Message] = field(default_factory=list)
33
+ created_at: float = field(default_factory=time.time)
34
+ last_active: float = field(default_factory=time.time)
35
+ metadata: Dict[str, Any] = field(default_factory=dict)
36
+ total_tokens: int = 0
37
+ total_cost: float = 0.0
38
+ turn_count: int = 0
39
+
40
+ def add_message(self, role: str, content: str, **kwargs) -> None:
41
+ """Add a message to the session."""
42
+ self.messages.append(Message(role=role, content=content, **kwargs))
43
+ self.last_active = time.time()
44
+ if role in ("user", "assistant"):
45
+ self.turn_count += 1
46
+
47
+ def get_history(self, strategy: str = "full", window_size: int = 20) -> List[Message]:
48
+ """Get conversation history based on strategy.
49
+
50
+ Args:
51
+ strategy: "full", "sliding_window", "last_n"
52
+ window_size: Number of messages for sliding window
53
+
54
+ Returns:
55
+ List of messages
56
+ """
57
+ if strategy == "full":
58
+ return list(self.messages)
59
+ elif strategy == "sliding_window":
60
+ return list(self.messages[-window_size:])
61
+ elif strategy == "last_n":
62
+ return list(self.messages[-window_size:])
63
+ return list(self.messages)
64
+
65
+ @property
66
+ def duration_seconds(self) -> float:
67
+ return self.last_active - self.created_at
68
+
69
+ @property
70
+ def is_empty(self) -> bool:
71
+ return len(self.messages) == 0
72
+
73
+ def to_dict(self) -> Dict[str, Any]:
74
+ return {
75
+ "session_id": self.session_id,
76
+ "agent_name": self.agent_name,
77
+ "tenant_id": self.tenant_id,
78
+ "turn_count": self.turn_count,
79
+ "total_tokens": self.total_tokens,
80
+ "total_cost": self.total_cost,
81
+ "created_at": self.created_at,
82
+ "last_active": self.last_active,
83
+ "message_count": len(self.messages),
84
+ }
85
+
86
+
87
+ class SessionManager:
88
+ """Manages agent sessions (in-memory for local, DynamoDB for prod).
89
+
90
+ Usage:
91
+ manager = SessionManager()
92
+ session = manager.get_or_create("session-123", "my-agent")
93
+ session.add_message("user", "Hello!")
94
+ manager.save(session)
95
+ """
96
+
97
+ def __init__(self, ttl_hours: int = 24, max_sessions: int = 10000):
98
+ self._sessions: Dict[str, Session] = {}
99
+ self.ttl_seconds = ttl_hours * 3600
100
+ self.max_sessions = max_sessions
101
+
102
+ def get_or_create(
103
+ self, session_id: Optional[str], agent_name: str, tenant_id: str = "default"
104
+ ) -> Session:
105
+ """Get existing session or create new one."""
106
+ if session_id and session_id in self._sessions:
107
+ session = self._sessions[session_id]
108
+ session.last_active = time.time()
109
+ return session
110
+
111
+ # Create new session
112
+ sid = session_id or f"sess-{uuid.uuid4().hex[:12]}"
113
+ session = Session(session_id=sid, agent_name=agent_name, tenant_id=tenant_id)
114
+ self._sessions[sid] = session
115
+ self._cleanup()
116
+ return session
117
+
118
+ def get(self, session_id: str) -> Optional[Session]:
119
+ """Get a session by ID."""
120
+ return self._sessions.get(session_id)
121
+
122
+ def save(self, session: Session) -> None:
123
+ """Save/update a session."""
124
+ self._sessions[session.session_id] = session
125
+
126
+ def delete(self, session_id: str) -> bool:
127
+ """Delete a session."""
128
+ if session_id in self._sessions:
129
+ del self._sessions[session_id]
130
+ return True
131
+ return False
132
+
133
+ def list_sessions(
134
+ self, agent_name: Optional[str] = None, tenant_id: Optional[str] = None
135
+ ) -> List[Session]:
136
+ """List sessions with optional filters."""
137
+ sessions = list(self._sessions.values())
138
+ if agent_name:
139
+ sessions = [s for s in sessions if s.agent_name == agent_name]
140
+ if tenant_id:
141
+ sessions = [s for s in sessions if s.tenant_id == tenant_id]
142
+ return sorted(sessions, key=lambda s: s.last_active, reverse=True)
143
+
144
+ def get_active_count(self) -> int:
145
+ """Count active (non-expired) sessions."""
146
+ cutoff = time.time() - self.ttl_seconds
147
+ return sum(1 for s in self._sessions.values() if s.last_active > cutoff)
148
+
149
+ def _cleanup(self) -> None:
150
+ """Remove expired sessions."""
151
+ cutoff = time.time() - self.ttl_seconds
152
+ expired = [sid for sid, s in self._sessions.items() if s.last_active < cutoff]
153
+ for sid in expired:
154
+ del self._sessions[sid]
File without changes
File without changes
@@ -0,0 +1,167 @@
1
+ """Tool registry and @Tool decorator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from typing import Any, Callable, Dict, List, Optional
10
+
11
+
12
+ class ToolPermission(Enum):
13
+ READ = "read"
14
+ WRITE = "write"
15
+ ADMIN = "admin"
16
+ EXECUTE = "execute"
17
+
18
+
19
+ @dataclass
20
+ class ToolDefinition:
21
+ """Metadata for a registered tool."""
22
+
23
+ name: str
24
+ description: str
25
+ fn: Callable
26
+ permissions: List[ToolPermission] = field(default_factory=lambda: [ToolPermission.READ])
27
+ timeout_seconds: int = 30
28
+ requires_approval: bool = False
29
+ version: str = "1.0.0"
30
+ parameters: Dict[str, Any] = field(default_factory=dict)
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ return {
34
+ "name": self.name,
35
+ "description": self.description,
36
+ "permissions": [p.value for p in self.permissions],
37
+ "timeout_seconds": self.timeout_seconds,
38
+ "requires_approval": self.requires_approval,
39
+ "version": self.version,
40
+ }
41
+
42
+
43
+ @dataclass
44
+ class ToolCallResult:
45
+ """Result of a tool execution."""
46
+
47
+ tool_name: str
48
+ success: bool
49
+ output: Any = None
50
+ error: Optional[str] = None
51
+ latency_ms: float = 0.0
52
+ timestamp: float = field(default_factory=time.time)
53
+
54
+
55
+ class ToolFunction:
56
+ """Wrapper around a Tool-decorated function."""
57
+
58
+ def __init__(self, fn: Callable, definition: ToolDefinition):
59
+ self.fn = fn
60
+ self.definition = definition
61
+ self.name = definition.name
62
+ self.description = definition.description
63
+ functools.update_wrapper(self, fn)
64
+
65
+ def __call__(self, *args, **kwargs) -> Any:
66
+ return self.fn(*args, **kwargs)
67
+
68
+ def execute(self, **kwargs) -> ToolCallResult:
69
+ """Execute the tool with tracking."""
70
+ start = time.time()
71
+ try:
72
+ output = self.fn(**kwargs)
73
+ return ToolCallResult(
74
+ tool_name=self.name,
75
+ success=True,
76
+ output=output,
77
+ latency_ms=(time.time() - start) * 1000,
78
+ )
79
+ except Exception as e:
80
+ return ToolCallResult(
81
+ tool_name=self.name,
82
+ success=False,
83
+ error=str(e),
84
+ latency_ms=(time.time() - start) * 1000,
85
+ )
86
+
87
+ def __repr__(self) -> str:
88
+ return f"Tool(name='{self.name}')"
89
+
90
+
91
+ def Tool(
92
+ description: str = "",
93
+ permissions: Optional[List[ToolPermission]] = None,
94
+ timeout_seconds: int = 30,
95
+ requires_approval: bool = False,
96
+ version: str = "1.0.0",
97
+ ) -> Callable:
98
+ """Decorator to register a function as an agent tool.
99
+
100
+ Usage:
101
+ @Tool(description="Search the knowledge base")
102
+ def search_kb(query: str) -> list:
103
+ return results
104
+ """
105
+ def decorator(fn: Callable) -> ToolFunction:
106
+ definition = ToolDefinition(
107
+ name=fn.__name__,
108
+ description=description or fn.__doc__ or "",
109
+ fn=fn,
110
+ permissions=permissions or [ToolPermission.READ],
111
+ timeout_seconds=timeout_seconds,
112
+ requires_approval=requires_approval,
113
+ version=version,
114
+ )
115
+ return ToolFunction(fn, definition)
116
+ return decorator
117
+
118
+
119
+ class ToolRegistry:
120
+ """Central registry of all available tools.
121
+
122
+ Usage:
123
+ registry = ToolRegistry()
124
+ registry.register(search_kb)
125
+ result = registry.execute("search_kb", query="hello")
126
+ """
127
+
128
+ def __init__(self):
129
+ self._tools: Dict[str, ToolFunction] = {}
130
+ self._call_log: List[ToolCallResult] = []
131
+
132
+ def register(self, tool: ToolFunction) -> None:
133
+ """Register a tool."""
134
+ self._tools[tool.name] = tool
135
+
136
+ def get(self, name: str) -> Optional[ToolFunction]:
137
+ """Get a tool by name."""
138
+ return self._tools.get(name)
139
+
140
+ def execute(self, name: str, **kwargs) -> ToolCallResult:
141
+ """Execute a tool by name."""
142
+ tool = self._tools.get(name)
143
+ if not tool:
144
+ return ToolCallResult(tool_name=name, success=False, error=f"Tool '{name}' not found")
145
+ result = tool.execute(**kwargs)
146
+ self._call_log.append(result)
147
+ return result
148
+
149
+ def list_tools(self) -> List[ToolDefinition]:
150
+ """List all registered tools."""
151
+ return [t.definition for t in self._tools.values()]
152
+
153
+ def is_allowed(self, tool_name: str, allowed_tools: Optional[List[str]] = None, denied_tools: Optional[List[str]] = None) -> bool:
154
+ """Check if a tool is allowed for a tenant."""
155
+ if denied_tools and tool_name in denied_tools:
156
+ return False
157
+ if allowed_tools and allowed_tools != ["all"] and tool_name not in allowed_tools:
158
+ return False
159
+ return True
160
+
161
+ @property
162
+ def call_log(self) -> List[ToolCallResult]:
163
+ return self._call_log
164
+
165
+ @property
166
+ def tool_count(self) -> int:
167
+ return len(self._tools)
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: substrai-agentdeploy
3
+ Version: 0.1.0
4
+ Summary: Zero-to-production AI agent deployment framework
5
+ Author-email: Gaurav Kumar Sinha <gaurav@substrai.dev>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/substrai/agentdeploy
8
+ Project-URL: Repository, https://github.com/substrai/agentdeploy
9
+ Keywords: ai-agent,deployment,serverless,lambda,llm,multi-tenancy,production
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: pyyaml>=6.0
14
+ Provides-Extra: aws
15
+ Requires-Dist: boto3>=1.28.0; extra == "aws"
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7.0; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # AgentDeploy
21
+
22
+ **Zero-to-production AI agent deployment framework.**
23
+
24
+ > Built by [SubstrAI](https://github.com/substrai) — Open-source GenAI frameworks for serverless infrastructure.
25
+
26
+ [![PyPI version](https://badge.fury.io/py/substrai-agentdeploy.svg)](https://pypi.org/project/substrai-agentdeploy/)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
28
+
29
+ ## The Problem
30
+
31
+ Building an AI agent is easy. Deploying it to production with auth, scaling, sessions, cost controls, and multi-tenancy takes weeks of custom infrastructure — every time.
32
+
33
+ ## The Solution
34
+
35
+ ```python
36
+ from agentdeploy import agent, Tool, Session
37
+
38
+ @Tool(description="Search knowledge base")
39
+ def search_kb(query: str) -> list:
40
+ return ["result 1", "result 2"]
41
+
42
+ @agent(name="support-agent", model="bedrock/claude-3-sonnet", tools=[search_kb])
43
+ def support_agent(message: str, session: Session) -> str:
44
+ return f"I can help with: {message}"
45
+ ```
46
+
47
+ ```bash
48
+ agentdeploy deploy --env prod
49
+ # ✓ Deployed: https://xxx.execute-api.us-east-1.amazonaws.com/prod/agent
50
+ ```
51
+
52
+ ## Features
53
+
54
+ - **@agent decorator** — turn any function into a deployable agent
55
+ - **Session management** — DynamoDB-backed conversation persistence
56
+ - **Tool sandboxing** — per-tenant tool permissions with audit trail
57
+ - **Cost circuit breakers** — auto-kill runs exceeding budget
58
+ - **Multi-tenancy** — tenant isolation, rate limiting, cost tracking
59
+ - **One-command deploy** — API Gateway + Lambda + DynamoDB
60
+ - **Provider agnostic** — works with Bedrock, OpenAI, Anthropic, custom
61
+ - **Adapter interface** — supports LangChain, CrewAI, Strands, custom agents
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install substrai-agentdeploy
67
+ ```
68
+
69
+ ## Quick Start
70
+
71
+ ```bash
72
+ agentdeploy init my-agent
73
+ cd my-agent
74
+ agentdeploy dev # local dev server
75
+ agentdeploy deploy --env prod
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT — see [LICENSE](LICENSE)
81
+
82
+ ## Author
83
+
84
+ **Gaurav Kumar Sinha** — Founder, [SubstrAI](https://github.com/substrai)
85
+
86
+ - Email: gaurav@substrai.dev
87
+ - GitHub: [@substrai](https://github.com/substrai)
@@ -0,0 +1,21 @@
1
+ agentdeploy/__init__.py,sha256=POWK2oIAFpb_F_9lwJRnUTt6dY-fLYttjzEVA3y0S9E,1101
2
+ agentdeploy/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ agentdeploy/adapters/base.py,sha256=yc2f3JCUY1nltMHAQY3bUIci07_yv2uOrI0sTH1Gtz4,1528
4
+ agentdeploy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ agentdeploy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ agentdeploy/core/agent.py,sha256=8q2sVjh5_MwnRM3BMwjr7pCgup19XuSXGkKals25LC8,3555
7
+ agentdeploy/core/runtime.py,sha256=CSfFXrWwDUhmYS1RpwW3nyElYC8LGFhPH3zxmmEw7C4,6338
8
+ agentdeploy/cost/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ agentdeploy/cost/enforcer.py,sha256=YtZ5_af3R3RRir_8TUniNwXW0RU2yUDth2FfflfyUKU,6290
10
+ agentdeploy/observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ agentdeploy/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ agentdeploy/session/manager.py,sha256=_RtsrJAETId5KYuAffS3-WDetDXibZUC6BV6WCf7wo0,5279
13
+ agentdeploy/tenants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ agentdeploy/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ agentdeploy/tools/registry.py,sha256=xif2j7usBI-KKTBx-vYx2-eORmfFrRROXhZogVVoBWU,4977
16
+ substrai_agentdeploy-0.1.0.dist-info/licenses/LICENSE,sha256=S-Ig-oAs_ELQwQdVSgfhQHgu00NMQ4O1E0dvgsQPBLI,830
17
+ substrai_agentdeploy-0.1.0.dist-info/METADATA,sha256=obGMb6vkvP-CqAKJ2Ln4X6DIBIcmCFrNJ1xTGJkZ4e8,2734
18
+ substrai_agentdeploy-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
19
+ substrai_agentdeploy-0.1.0.dist-info/entry_points.txt,sha256=D7VNYorBu05d66GUwb66-VAP7AL-sw3TdstbOVEvJkc,58
20
+ substrai_agentdeploy-0.1.0.dist-info/top_level.txt,sha256=EA5y-wqe22J2XIJ4XEuRKZXABVM1PVmmDzu0FIo14lU,12
21
+ substrai_agentdeploy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agentdeploy = agentdeploy.cli.main:main
@@ -0,0 +1,17 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gaurav Kumar Sinha (Substrai AI)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@@ -0,0 +1 @@
1
+ agentdeploy