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.
- agentdeploy/__init__.py +38 -0
- agentdeploy/adapters/__init__.py +0 -0
- agentdeploy/adapters/base.py +60 -0
- agentdeploy/cli/__init__.py +0 -0
- agentdeploy/core/__init__.py +0 -0
- agentdeploy/core/agent.py +109 -0
- agentdeploy/core/runtime.py +191 -0
- agentdeploy/cost/__init__.py +0 -0
- agentdeploy/cost/enforcer.py +173 -0
- agentdeploy/observability/__init__.py +0 -0
- agentdeploy/session/__init__.py +0 -0
- agentdeploy/session/manager.py +154 -0
- agentdeploy/tenants/__init__.py +0 -0
- agentdeploy/tools/__init__.py +0 -0
- agentdeploy/tools/registry.py +167 -0
- substrai_agentdeploy-0.1.0.dist-info/METADATA +87 -0
- substrai_agentdeploy-0.1.0.dist-info/RECORD +21 -0
- substrai_agentdeploy-0.1.0.dist-info/WHEEL +5 -0
- substrai_agentdeploy-0.1.0.dist-info/entry_points.txt +2 -0
- substrai_agentdeploy-0.1.0.dist-info/licenses/LICENSE +17 -0
- substrai_agentdeploy-0.1.0.dist-info/top_level.txt +1 -0
agentdeploy/__init__.py
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/substrai-agentdeploy/)
|
|
27
|
+
[](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,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
|