agentscope-runtime 1.0.2__py3-none-any.whl → 1.0.4__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.
- agentscope_runtime/adapters/agentscope/stream.py +2 -9
- agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
- agentscope_runtime/adapters/ms_agent_framework/message.py +205 -0
- agentscope_runtime/adapters/ms_agent_framework/stream.py +418 -0
- agentscope_runtime/adapters/utils.py +6 -0
- agentscope_runtime/cli/commands/deploy.py +383 -0
- agentscope_runtime/common/collections/redis_mapping.py +4 -1
- agentscope_runtime/common/container_clients/knative_client.py +466 -0
- agentscope_runtime/engine/__init__.py +4 -0
- agentscope_runtime/engine/app/agent_app.py +48 -5
- agentscope_runtime/engine/constant.py +1 -0
- agentscope_runtime/engine/deployers/__init__.py +12 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +31 -1
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +458 -41
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +76 -0
- agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +749 -0
- agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
- agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
- agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
- agentscope_runtime/engine/deployers/kubernetes_deployer.py +3 -0
- agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +8 -2
- agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +5 -0
- agentscope_runtime/engine/deployers/utils/net_utils.py +65 -0
- agentscope_runtime/engine/runner.py +17 -3
- agentscope_runtime/engine/schemas/exception.py +24 -0
- agentscope_runtime/engine/services/agent_state/redis_state_service.py +61 -8
- agentscope_runtime/engine/services/agent_state/state_service_factory.py +2 -5
- agentscope_runtime/engine/services/memory/redis_memory_service.py +129 -25
- agentscope_runtime/engine/services/session_history/redis_session_history_service.py +160 -34
- agentscope_runtime/engine/tracing/wrapper.py +18 -4
- agentscope_runtime/sandbox/__init__.py +14 -6
- agentscope_runtime/sandbox/box/base/__init__.py +2 -2
- agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
- agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
- agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
- agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
- agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
- agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
- agentscope_runtime/sandbox/box/sandbox.py +98 -65
- agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
- agentscope_runtime/sandbox/build.py +50 -57
- agentscope_runtime/sandbox/client/__init__.py +6 -1
- agentscope_runtime/sandbox/client/async_http_client.py +339 -0
- agentscope_runtime/sandbox/client/base.py +74 -0
- agentscope_runtime/sandbox/client/http_client.py +108 -329
- agentscope_runtime/sandbox/enums.py +7 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +264 -4
- agentscope_runtime/sandbox/manager/server/app.py +7 -1
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/METADATA +109 -29
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/RECORD +58 -46
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/WHEEL +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, Dict, List, Union, Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from .utils.docker_image_utils import (
|
|
8
|
+
ImageFactory,
|
|
9
|
+
RegistryConfig,
|
|
10
|
+
)
|
|
11
|
+
from .adapter.protocol_adapter import ProtocolAdapter
|
|
12
|
+
from .base import DeployManager
|
|
13
|
+
from ...common.container_clients.knative_client import (
|
|
14
|
+
KnativeClient,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class K8sConfig(BaseModel):
|
|
21
|
+
# Kubernetes settings
|
|
22
|
+
k8s_namespace: Optional[str] = Field(
|
|
23
|
+
"agentscope-runtime",
|
|
24
|
+
description="Kubernetes namespace to deploy KService. ",
|
|
25
|
+
)
|
|
26
|
+
kubeconfig_path: Optional[str] = Field(
|
|
27
|
+
None,
|
|
28
|
+
description="Path to kubeconfig file. If not set, will try "
|
|
29
|
+
"in-cluster config or default kubeconfig.",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BuildConfig(BaseModel):
|
|
34
|
+
"""Build configuration"""
|
|
35
|
+
|
|
36
|
+
build_context_dir: str = "/tmp/k8s_build"
|
|
37
|
+
dockerfile_template: str = None
|
|
38
|
+
build_timeout: int = 600 # 10 minutes
|
|
39
|
+
push_timeout: int = 300 # 5 minutes
|
|
40
|
+
cleanup_after_build: bool = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class KnativeDeployManager(DeployManager):
|
|
44
|
+
"""
|
|
45
|
+
Deploy an AgentScope runner as a Knative Service.
|
|
46
|
+
Requires a Kubernetes cluster with Knative Serving installed.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
kube_config: K8sConfig = None,
|
|
52
|
+
registry_config: RegistryConfig = RegistryConfig(),
|
|
53
|
+
build_context_dir: str = "/tmp/k8s_build",
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize the Knative deployer.
|
|
57
|
+
"""
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.kubeconfig = kube_config
|
|
60
|
+
self.registry_config = registry_config
|
|
61
|
+
self.image_factory = ImageFactory()
|
|
62
|
+
self.build_context_dir = build_context_dir
|
|
63
|
+
self._deployed_resources = {}
|
|
64
|
+
self._built_images = {}
|
|
65
|
+
|
|
66
|
+
self.knative_client = KnativeClient(
|
|
67
|
+
config=self.kubeconfig,
|
|
68
|
+
image_registry=self.registry_config.get_full_url(),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def deploy(
|
|
72
|
+
self,
|
|
73
|
+
app=None,
|
|
74
|
+
runner=None,
|
|
75
|
+
stream: bool = True,
|
|
76
|
+
protocol_adapters: Optional[list[ProtocolAdapter]] = None,
|
|
77
|
+
requirements: Optional[Union[str, List[str]]] = None,
|
|
78
|
+
extra_packages: Optional[List[str]] = None,
|
|
79
|
+
base_image: str = "python:3.9-slim",
|
|
80
|
+
environment: Dict = None,
|
|
81
|
+
runtime_config: Dict = None,
|
|
82
|
+
annotations: Dict = None,
|
|
83
|
+
labels: Dict = None,
|
|
84
|
+
port: int = 8080,
|
|
85
|
+
mount_dir: str = None,
|
|
86
|
+
image_name: str = "agent_llm",
|
|
87
|
+
image_tag: str = "latest",
|
|
88
|
+
push_to_registry: bool = False,
|
|
89
|
+
**kwargs,
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Deploy the runner as a Knative Service.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
app: Agent app to be deployed
|
|
96
|
+
runner: Complete Runner object with agent, environment_manager,
|
|
97
|
+
context_manager
|
|
98
|
+
stream: Enable streaming responses
|
|
99
|
+
protocol_adapters: protocol adapters
|
|
100
|
+
requirements: PyPI dependencies (following _agent_engines.py
|
|
101
|
+
pattern)
|
|
102
|
+
extra_packages: User code directory/file path
|
|
103
|
+
base_image: Docker base image
|
|
104
|
+
port: Container port
|
|
105
|
+
environment: Environment variables dict
|
|
106
|
+
mount_dir: Mount directory
|
|
107
|
+
runtime_config: K8s runtime configuration
|
|
108
|
+
annotations: knative service annotations
|
|
109
|
+
labels: knative service labels
|
|
110
|
+
# Backward compatibility
|
|
111
|
+
image_name: Image name
|
|
112
|
+
image_tag: Image tag
|
|
113
|
+
push_to_registry: Push to registry
|
|
114
|
+
**kwargs: Additional arguments
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dict containing deploy_id, url, resource_name
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
RuntimeError: If kservice fails
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
created_resources = []
|
|
124
|
+
deploy_id = self.deploy_id
|
|
125
|
+
try:
|
|
126
|
+
logger.info(f"Starting Knative Service {deploy_id}")
|
|
127
|
+
|
|
128
|
+
# Step 1: Build image with proper error handling
|
|
129
|
+
logger.info("Building runner image...")
|
|
130
|
+
try:
|
|
131
|
+
built_image_name = self.image_factory.build_image(
|
|
132
|
+
app=app,
|
|
133
|
+
runner=runner,
|
|
134
|
+
base_image=base_image,
|
|
135
|
+
build_context_dir=self.build_context_dir,
|
|
136
|
+
registry_config=self.registry_config,
|
|
137
|
+
image_name=image_name,
|
|
138
|
+
image_tag=image_tag,
|
|
139
|
+
push_to_registry=push_to_registry,
|
|
140
|
+
port=port,
|
|
141
|
+
protocol_adapters=protocol_adapters,
|
|
142
|
+
**kwargs,
|
|
143
|
+
)
|
|
144
|
+
if not built_image_name:
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
"Image build failed - no image name returned",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
created_resources.append(f"image:{built_image_name}")
|
|
150
|
+
self._built_images[deploy_id] = built_image_name
|
|
151
|
+
logger.info(f"Image built successfully: {built_image_name}")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Image build failed: {e}")
|
|
154
|
+
raise RuntimeError(f"Failed to build image: {e}") from e
|
|
155
|
+
|
|
156
|
+
if mount_dir:
|
|
157
|
+
if not os.path.isabs(mount_dir):
|
|
158
|
+
mount_dir = os.path.abspath(mount_dir)
|
|
159
|
+
|
|
160
|
+
volume_bindings = {
|
|
161
|
+
mount_dir: {
|
|
162
|
+
"bind": mount_dir,
|
|
163
|
+
"mode": "rw",
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
else:
|
|
167
|
+
volume_bindings = {}
|
|
168
|
+
|
|
169
|
+
resource_name = self.get_resource_name(deploy_id)
|
|
170
|
+
|
|
171
|
+
logger.info(f"Building Knative Service for {deploy_id}")
|
|
172
|
+
|
|
173
|
+
# Create Knative Service
|
|
174
|
+
name, url = self.knative_client.create_kservice(
|
|
175
|
+
name=resource_name,
|
|
176
|
+
image=built_image_name,
|
|
177
|
+
ports=[port],
|
|
178
|
+
volumes=volume_bindings,
|
|
179
|
+
environment=environment,
|
|
180
|
+
runtime_config=runtime_config or {},
|
|
181
|
+
annotations=annotations or {},
|
|
182
|
+
labels=labels or {},
|
|
183
|
+
)
|
|
184
|
+
if not url:
|
|
185
|
+
import traceback
|
|
186
|
+
|
|
187
|
+
raise RuntimeError(
|
|
188
|
+
f"Failed to create resource: "
|
|
189
|
+
f"{resource_name}, {traceback.format_exc()}",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
logger.info(f"Knative Service url {url} successful")
|
|
193
|
+
self._deployed_resources[deploy_id] = {
|
|
194
|
+
"resource_name": name,
|
|
195
|
+
"config": {
|
|
196
|
+
"runner": runner.__class__.__name__,
|
|
197
|
+
"extra_packages": extra_packages,
|
|
198
|
+
"requirements": requirements, # New format
|
|
199
|
+
"base_image": base_image,
|
|
200
|
+
"port": port,
|
|
201
|
+
"environment": environment,
|
|
202
|
+
"runtime_config": runtime_config,
|
|
203
|
+
"stream": stream,
|
|
204
|
+
"protocol_adapters": protocol_adapters,
|
|
205
|
+
**kwargs,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
"deploy_id": deploy_id,
|
|
210
|
+
"resource_name": resource_name,
|
|
211
|
+
"url": url,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
import traceback
|
|
216
|
+
|
|
217
|
+
logger.error(f"Knative Service {deploy_id} failed: {e}")
|
|
218
|
+
# Enhanced rollback with better error handling
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
f"Knative Service failed: {e}, {traceback.format_exc()}",
|
|
221
|
+
) from e
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def get_resource_name(deploy_id: str) -> str:
|
|
225
|
+
return f"agent-{deploy_id[:8]}"
|
|
226
|
+
|
|
227
|
+
async def stop(
|
|
228
|
+
self,
|
|
229
|
+
deploy_id: str,
|
|
230
|
+
**kwargs,
|
|
231
|
+
) -> Dict[str, Any]:
|
|
232
|
+
"""Stop Knative Service.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
deploy_id: Deployment identifier
|
|
236
|
+
**kwargs: Additional parameters
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Dict with success status, message, and details
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
resource_name = self.get_resource_name(deploy_id)
|
|
243
|
+
try:
|
|
244
|
+
# Try to remove the KService
|
|
245
|
+
success = self.knative_client.delete_kservice(resource_name)
|
|
246
|
+
|
|
247
|
+
if success:
|
|
248
|
+
return {
|
|
249
|
+
"success": True,
|
|
250
|
+
"message": f"Knative deployment {resource_name} "
|
|
251
|
+
f"removed",
|
|
252
|
+
"details": {
|
|
253
|
+
"deploy_id": deploy_id,
|
|
254
|
+
"resource_name": resource_name,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
else:
|
|
258
|
+
return {
|
|
259
|
+
"success": False,
|
|
260
|
+
"message": f"Knative deployment {resource_name} not "
|
|
261
|
+
f"found (may already be deleted), Please check the "
|
|
262
|
+
f"detail in cluster",
|
|
263
|
+
"details": {
|
|
264
|
+
"deploy_id": deploy_id,
|
|
265
|
+
"resource_name": resource_name,
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(
|
|
270
|
+
f"Failed to remove Knative service {resource_name}: {e}",
|
|
271
|
+
)
|
|
272
|
+
return {
|
|
273
|
+
"success": False,
|
|
274
|
+
"message": f"Failed to remove Knative service: {e}",
|
|
275
|
+
"details": {
|
|
276
|
+
"deploy_id": deploy_id,
|
|
277
|
+
"resource_name": resource_name,
|
|
278
|
+
"error": str(e),
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
def get_status(self) -> str:
|
|
283
|
+
"""Get KService status"""
|
|
284
|
+
if self.deploy_id not in self._deployed_resources:
|
|
285
|
+
return "not_found"
|
|
286
|
+
|
|
287
|
+
resources = self._deployed_resources[self.deploy_id]
|
|
288
|
+
kservice_name = resources["resource_name"]
|
|
289
|
+
|
|
290
|
+
return self.knative_client.get_kservice_status(kservice_name)
|
|
@@ -144,6 +144,7 @@ class KubernetesDeployManager(DeployManager):
|
|
|
144
144
|
image_tag: str = "latest",
|
|
145
145
|
push_to_registry: bool = False,
|
|
146
146
|
use_cache: bool = True,
|
|
147
|
+
pypi_mirror: Optional[str] = None,
|
|
147
148
|
**kwargs,
|
|
148
149
|
) -> Dict[str, Any]:
|
|
149
150
|
"""
|
|
@@ -170,6 +171,7 @@ class KubernetesDeployManager(DeployManager):
|
|
|
170
171
|
mount_dir: Mount directory
|
|
171
172
|
runtime_config: K8s runtime configuration
|
|
172
173
|
use_cache: Enable build cache (default: True)
|
|
174
|
+
pypi_mirror: PyPI mirror URL for pip package installation
|
|
173
175
|
# Backward compatibility
|
|
174
176
|
image_name: Image name
|
|
175
177
|
image_tag: Image tag
|
|
@@ -209,6 +211,7 @@ class KubernetesDeployManager(DeployManager):
|
|
|
209
211
|
protocol_adapters=protocol_adapters,
|
|
210
212
|
custom_endpoints=custom_endpoints,
|
|
211
213
|
use_cache=use_cache,
|
|
214
|
+
pypi_mirror=pypi_mirror,
|
|
212
215
|
**kwargs,
|
|
213
216
|
)
|
|
214
217
|
if not built_image_name:
|
|
@@ -22,6 +22,7 @@ class DockerfileConfig(BaseModel):
|
|
|
22
22
|
health_check_endpoint: str = "/health"
|
|
23
23
|
custom_template: Optional[str] = None
|
|
24
24
|
platform: Optional[str] = None
|
|
25
|
+
pypi_mirror: Optional[str] = None
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class DockerfileGenerator:
|
|
@@ -73,8 +74,7 @@ COPY . {working_dir}/
|
|
|
73
74
|
# Install Python dependencies
|
|
74
75
|
RUN pip install --no-cache-dir --upgrade pip
|
|
75
76
|
RUN if [ -f requirements.txt ]; then \\
|
|
76
|
-
pip install --no-cache-dir -r requirements.txt
|
|
77
|
-
-i https://pypi.tuna.tsinghua.edu.cn/simple; fi
|
|
77
|
+
pip install --no-cache-dir -r requirements.txt{pypi_mirror_flag}; fi
|
|
78
78
|
|
|
79
79
|
# Create non-root user for security
|
|
80
80
|
RUN adduser --disabled-password --gecos '' {user} && \\
|
|
@@ -136,6 +136,11 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
|
|
|
136
136
|
f'"--port", "{config.port}"]'
|
|
137
137
|
)
|
|
138
138
|
|
|
139
|
+
# Prepare PyPI mirror flag
|
|
140
|
+
pypi_mirror_flag = ""
|
|
141
|
+
if config.pypi_mirror:
|
|
142
|
+
pypi_mirror_flag = f" -i {config.pypi_mirror}"
|
|
143
|
+
|
|
139
144
|
# Format template with configuration values
|
|
140
145
|
content = template.format(
|
|
141
146
|
base_image=config.base_image,
|
|
@@ -147,6 +152,7 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
|
|
|
147
152
|
env_vars_section=env_vars_section,
|
|
148
153
|
startup_command_section=startup_command_section,
|
|
149
154
|
platform=config.platform,
|
|
155
|
+
pypi_mirror_flag=pypi_mirror_flag,
|
|
150
156
|
)
|
|
151
157
|
|
|
152
158
|
return content
|
|
@@ -40,6 +40,7 @@ class ImageConfig(BaseModel):
|
|
|
40
40
|
port: int = 8000
|
|
41
41
|
env_vars: Dict[str, str] = Field(default_factory=lambda: {})
|
|
42
42
|
startup_command: Optional[str] = None
|
|
43
|
+
pypi_mirror: Optional[str] = None
|
|
43
44
|
|
|
44
45
|
# Runtime configuration
|
|
45
46
|
host: str = "0.0.0.0" # Container-friendly default
|
|
@@ -218,6 +219,7 @@ class ImageFactory:
|
|
|
218
219
|
env_vars=config.env_vars,
|
|
219
220
|
startup_command=startup_command,
|
|
220
221
|
platform=config.platform,
|
|
222
|
+
pypi_mirror=config.pypi_mirror,
|
|
221
223
|
)
|
|
222
224
|
|
|
223
225
|
dockerfile_path = self.dockerfile_generator.create_dockerfile(
|
|
@@ -314,6 +316,7 @@ class ImageFactory:
|
|
|
314
316
|
embed_task_processor: bool = True,
|
|
315
317
|
extra_startup_args: Optional[Dict[str, Union[str, int, bool]]] = None,
|
|
316
318
|
use_cache: bool = True,
|
|
319
|
+
pypi_mirror: Optional[str] = None,
|
|
317
320
|
**kwargs,
|
|
318
321
|
) -> str:
|
|
319
322
|
"""
|
|
@@ -339,6 +342,7 @@ class ImageFactory:
|
|
|
339
342
|
embed_task_processor: Whether to embed task processor
|
|
340
343
|
extra_startup_args: Additional startup arguments
|
|
341
344
|
use_cache: Enable build cache (default: True)
|
|
345
|
+
pypi_mirror: PyPI mirror URL for pip package installation
|
|
342
346
|
**kwargs: Additional configuration options
|
|
343
347
|
|
|
344
348
|
Returns:
|
|
@@ -373,6 +377,7 @@ class ImageFactory:
|
|
|
373
377
|
host=host,
|
|
374
378
|
embed_task_processor=embed_task_processor,
|
|
375
379
|
extra_startup_args=extra_startup_args or {},
|
|
380
|
+
pypi_mirror=pypi_mirror,
|
|
376
381
|
**kwargs,
|
|
377
382
|
)
|
|
378
383
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import ipaddress
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import psutil
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_first_non_loopback_ip() -> Optional[str]:
|
|
11
|
+
"""Get the first non-loopback IP address from network interfaces.
|
|
12
|
+
|
|
13
|
+
- Selects the interface with the lowest index
|
|
14
|
+
- Only considers interfaces that are up
|
|
15
|
+
- Supports IPv4/IPv6 based on environment variable
|
|
16
|
+
- Falls back to socket.gethostbyname() if no address found
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str | None: The first non-loopback IP address, or None if not found
|
|
20
|
+
"""
|
|
21
|
+
result = None
|
|
22
|
+
lowest_index = float("inf")
|
|
23
|
+
|
|
24
|
+
use_ipv6 = os.environ.get("USE_IPV6", "false").lower() == "true"
|
|
25
|
+
target_family = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
26
|
+
|
|
27
|
+
net_if_stats = psutil.net_if_stats()
|
|
28
|
+
|
|
29
|
+
for index, (interface, addrs) in enumerate(
|
|
30
|
+
psutil.net_if_addrs().items(),
|
|
31
|
+
):
|
|
32
|
+
stats = net_if_stats.get(interface)
|
|
33
|
+
if stats is None or not stats.isup:
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
if index < lowest_index or result is None:
|
|
37
|
+
lowest_index = index
|
|
38
|
+
else:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
for addr in addrs:
|
|
42
|
+
if addr.family != target_family:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
ip_obj = ipaddress.ip_address(
|
|
47
|
+
addr.address.split("%")[0],
|
|
48
|
+
)
|
|
49
|
+
if ip_obj.is_loopback:
|
|
50
|
+
continue
|
|
51
|
+
result = addr.address
|
|
52
|
+
except ValueError:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
if result is not None:
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
hostname = socket.gethostname()
|
|
60
|
+
fallback_ip = socket.gethostbyname(hostname)
|
|
61
|
+
return fallback_ip
|
|
62
|
+
except socket.error:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
return None
|
|
@@ -272,6 +272,18 @@ class Runner:
|
|
|
272
272
|
kwargs.update(
|
|
273
273
|
{"msgs": await message_to_agno_message(request.input)},
|
|
274
274
|
)
|
|
275
|
+
elif self.framework_type == "ms_agent_framework":
|
|
276
|
+
from ..adapters.ms_agent_framework.stream import (
|
|
277
|
+
adapt_ms_agent_framework_message_stream,
|
|
278
|
+
)
|
|
279
|
+
from ..adapters.ms_agent_framework.message import (
|
|
280
|
+
message_to_ms_agent_framework_message,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
stream_adapter = adapt_ms_agent_framework_message_stream
|
|
284
|
+
kwargs.update(
|
|
285
|
+
{"msgs": message_to_ms_agent_framework_message(request.input)},
|
|
286
|
+
)
|
|
275
287
|
# TODO: support other frameworks
|
|
276
288
|
else:
|
|
277
289
|
|
|
@@ -282,6 +294,7 @@ class Runner:
|
|
|
282
294
|
|
|
283
295
|
stream_adapter = identity_stream_adapter
|
|
284
296
|
|
|
297
|
+
error = None
|
|
285
298
|
try:
|
|
286
299
|
async for event in stream_adapter(
|
|
287
300
|
source_stream=self._call_handler_streaming(
|
|
@@ -301,8 +314,6 @@ class Runner:
|
|
|
301
314
|
e = UnknownAgentException(original_exception=e)
|
|
302
315
|
error = Error(code=e.code, message=e.message)
|
|
303
316
|
logger.error(f"{error.model_dump()}: {traceback.format_exc()}")
|
|
304
|
-
yield seq_gen.yield_with_sequence(response.failed(error))
|
|
305
|
-
return
|
|
306
317
|
|
|
307
318
|
# Obtain token usage
|
|
308
319
|
try:
|
|
@@ -312,4 +323,7 @@ class Runner:
|
|
|
312
323
|
# Avoid empty message
|
|
313
324
|
pass
|
|
314
325
|
|
|
315
|
-
|
|
326
|
+
if error:
|
|
327
|
+
yield seq_gen.yield_with_sequence(response.failed(error))
|
|
328
|
+
else:
|
|
329
|
+
yield seq_gen.yield_with_sequence(response.completed())
|
|
@@ -578,3 +578,27 @@ class UnknownAgentException(AgentRuntimeErrorException):
|
|
|
578
578
|
message,
|
|
579
579
|
details,
|
|
580
580
|
)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class ModelQuotaExceededException(AgentRuntimeErrorException):
|
|
584
|
+
"""Model quota exceeded"""
|
|
585
|
+
|
|
586
|
+
def __init__(
|
|
587
|
+
self,
|
|
588
|
+
model_name: str,
|
|
589
|
+
details: Optional[Dict[str, Any]] = None,
|
|
590
|
+
):
|
|
591
|
+
message = f"Model quota exceeded: {model_name}"
|
|
592
|
+
super().__init__("MODEL_QUOTA_EXCEEDED", message, details)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class ModelContextLengthExceededException(AgentRuntimeErrorException):
|
|
596
|
+
"""Model context length exceeded"""
|
|
597
|
+
|
|
598
|
+
def __init__(
|
|
599
|
+
self,
|
|
600
|
+
model_name: str,
|
|
601
|
+
details: Optional[Dict[str, Any]] = None,
|
|
602
|
+
):
|
|
603
|
+
message = f"Model context length exceeded: {model_name}"
|
|
604
|
+
super().__init__("MODEL_CONTEXT_LENGTH_EXCEEDED", message, details)
|
|
@@ -21,29 +21,68 @@ class RedisStateService(StateService):
|
|
|
21
21
|
self,
|
|
22
22
|
redis_url: str = "redis://localhost:6379/0",
|
|
23
23
|
redis_client: Optional[aioredis.Redis] = None,
|
|
24
|
+
socket_timeout: Optional[float] = 5.0,
|
|
25
|
+
socket_connect_timeout: Optional[float] = 5.0,
|
|
26
|
+
max_connections: Optional[int] = None,
|
|
27
|
+
retry_on_timeout: bool = True,
|
|
28
|
+
ttl_seconds: Optional[int] = 3600, # 1 hour in seconds
|
|
29
|
+
health_check_interval: Optional[float] = 30.0,
|
|
30
|
+
socket_keepalive: bool = True,
|
|
24
31
|
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize RedisStateService.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
redis_url: Redis connection URL
|
|
37
|
+
redis_client: Optional pre-configured Redis client
|
|
38
|
+
socket_timeout: Socket timeout in seconds (default: 5.0)
|
|
39
|
+
socket_connect_timeout: Socket connect timeout in seconds
|
|
40
|
+
(default: 5.0)
|
|
41
|
+
max_connections: Maximum number of connections in the pool
|
|
42
|
+
(default: None)
|
|
43
|
+
retry_on_timeout: Whether to retry on timeout (default: True)
|
|
44
|
+
ttl_seconds: Time-to-live in seconds for state data. If None,
|
|
45
|
+
data never expires (default: 3600, i.e., 1 hour)
|
|
46
|
+
health_check_interval: Interval in seconds for health checks on
|
|
47
|
+
idle connections (default: 30.0).
|
|
48
|
+
Connections idle longer than this will be checked before reuse.
|
|
49
|
+
Set to 0 to disable.
|
|
50
|
+
socket_keepalive: Enable TCP keepalive to prevent
|
|
51
|
+
silent disconnections (default: True)
|
|
52
|
+
"""
|
|
25
53
|
self._redis_url = redis_url
|
|
26
54
|
self._redis = redis_client
|
|
27
|
-
self.
|
|
55
|
+
self._socket_timeout = socket_timeout
|
|
56
|
+
self._socket_connect_timeout = socket_connect_timeout
|
|
57
|
+
self._max_connections = max_connections
|
|
58
|
+
self._retry_on_timeout = retry_on_timeout
|
|
59
|
+
self._ttl_seconds = ttl_seconds
|
|
60
|
+
self._health_check_interval = health_check_interval
|
|
61
|
+
self._socket_keepalive = socket_keepalive
|
|
28
62
|
|
|
29
63
|
async def start(self) -> None:
|
|
30
|
-
"""
|
|
64
|
+
"""Starts the Redis connection with proper timeout and connection
|
|
65
|
+
pool settings."""
|
|
31
66
|
if self._redis is None:
|
|
32
67
|
self._redis = aioredis.from_url(
|
|
33
68
|
self._redis_url,
|
|
34
69
|
decode_responses=True,
|
|
70
|
+
socket_timeout=self._socket_timeout,
|
|
71
|
+
socket_connect_timeout=self._socket_connect_timeout,
|
|
72
|
+
max_connections=self._max_connections,
|
|
73
|
+
retry_on_timeout=self._retry_on_timeout,
|
|
74
|
+
health_check_interval=self._health_check_interval,
|
|
75
|
+
socket_keepalive=self._socket_keepalive,
|
|
35
76
|
)
|
|
36
|
-
self._health = True
|
|
37
77
|
|
|
38
78
|
async def stop(self) -> None:
|
|
39
|
-
"""
|
|
79
|
+
"""Closes the Redis connection."""
|
|
40
80
|
if self._redis:
|
|
41
|
-
await self._redis.
|
|
81
|
+
await self._redis.aclose()
|
|
42
82
|
self._redis = None
|
|
43
|
-
self._health = False
|
|
44
83
|
|
|
45
84
|
async def health(self) -> bool:
|
|
46
|
-
"""
|
|
85
|
+
"""Checks the health of the service."""
|
|
47
86
|
if not self._redis:
|
|
48
87
|
return False
|
|
49
88
|
try:
|
|
@@ -81,6 +120,11 @@ class RedisStateService(StateService):
|
|
|
81
120
|
round_id = 1
|
|
82
121
|
|
|
83
122
|
await self._redis.hset(key, round_id, json.dumps(state))
|
|
123
|
+
|
|
124
|
+
# Set TTL for the state key if configured
|
|
125
|
+
if self._ttl_seconds is not None:
|
|
126
|
+
await self._redis.expire(key, self._ttl_seconds)
|
|
127
|
+
|
|
84
128
|
return round_id
|
|
85
129
|
|
|
86
130
|
async def export_state(
|
|
@@ -110,4 +154,13 @@ class RedisStateService(StateService):
|
|
|
110
154
|
|
|
111
155
|
if state_json is None:
|
|
112
156
|
return None
|
|
113
|
-
|
|
157
|
+
|
|
158
|
+
# Refresh TTL when accessing the state
|
|
159
|
+
if self._ttl_seconds is not None:
|
|
160
|
+
await self._redis.expire(key, self._ttl_seconds)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
return json.loads(state_json)
|
|
164
|
+
except json.JSONDecodeError:
|
|
165
|
+
# Return None for corrupted state data instead of raising exception
|
|
166
|
+
return None
|
|
@@ -43,13 +43,10 @@ class StateServiceFactory(ServiceFactory[StateService]):
|
|
|
43
43
|
|
|
44
44
|
StateServiceFactory.register_backend(
|
|
45
45
|
"in_memory",
|
|
46
|
-
|
|
46
|
+
InMemoryStateService,
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
StateServiceFactory.register_backend(
|
|
50
50
|
"redis",
|
|
51
|
-
|
|
52
|
-
redis_url=kwargs.get("redis_url", "redis://localhost:6379/0"),
|
|
53
|
-
redis_client=kwargs.get("redis_client"),
|
|
54
|
-
),
|
|
51
|
+
RedisStateService,
|
|
55
52
|
)
|