agentscope-runtime 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. agentscope_runtime/engine/agents/agentscope_agent/agent.py +1 -0
  2. agentscope_runtime/engine/agents/agno_agent.py +1 -0
  3. agentscope_runtime/engine/agents/autogen_agent.py +245 -0
  4. agentscope_runtime/engine/schemas/agent_schemas.py +1 -1
  5. agentscope_runtime/engine/services/memory_service.py +2 -2
  6. agentscope_runtime/engine/services/redis_memory_service.py +187 -0
  7. agentscope_runtime/engine/services/redis_session_history_service.py +155 -0
  8. agentscope_runtime/sandbox/build.py +1 -1
  9. agentscope_runtime/sandbox/custom/custom_sandbox.py +0 -1
  10. agentscope_runtime/sandbox/custom/example.py +0 -1
  11. agentscope_runtime/sandbox/manager/container_clients/__init__.py +2 -0
  12. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +246 -4
  13. agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +550 -0
  14. agentscope_runtime/sandbox/manager/sandbox_manager.py +21 -82
  15. agentscope_runtime/sandbox/manager/server/app.py +55 -24
  16. agentscope_runtime/sandbox/manager/server/config.py +28 -16
  17. agentscope_runtime/sandbox/model/container.py +3 -1
  18. agentscope_runtime/sandbox/model/manager_config.py +19 -2
  19. agentscope_runtime/sandbox/tools/tool.py +111 -0
  20. agentscope_runtime/version.py +1 -1
  21. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/METADATA +74 -13
  22. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/RECORD +26 -23
  23. agentscope_runtime/sandbox/manager/utils.py +0 -78
  24. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/WHEEL +0 -0
  25. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/entry_points.txt +0 -0
  26. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/licenses/LICENSE +0 -0
  27. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/top_level.txt +0 -0
@@ -4,13 +4,12 @@ import logging
4
4
  import os
5
5
  import secrets
6
6
  import inspect
7
- import socket
8
7
  import traceback
9
8
 
10
9
  from functools import wraps
11
10
  from typing import Optional, Dict
12
- from uuid import uuid4
13
11
 
12
+ import shortuuid
14
13
  import requests
15
14
 
16
15
  from ..model import (
@@ -22,20 +21,17 @@ from ..enums import SandboxType
22
21
  from ..registry import SandboxRegistry
23
22
  from ..client import SandboxHttpClient, TrainingSandboxClient
24
23
 
25
- from ..manager.utils import is_port_available, sweep_port
26
24
  from ..manager.collections import (
27
25
  RedisMapping,
28
- RedisSetCollection,
29
26
  RedisQueue,
30
27
  InMemoryMapping,
31
28
  InMemoryQueue,
32
- InMemorySetCollection,
33
29
  )
34
30
  from ..manager.storage import (
35
31
  LocalStorage,
36
32
  OSSStorage,
37
33
  )
38
- from ..manager.container_clients import DockerClient
34
+ from ..manager.container_clients import DockerClient, KubernetesClient
39
35
  from ..constant import BROWSER_SESSION_ID
40
36
 
41
37
  logging.basicConfig(level=logging.INFO)
@@ -136,32 +132,26 @@ class SandboxManager:
136
132
  ) from e
137
133
 
138
134
  self.container_mapping = RedisMapping(redis_client)
139
- self.port_set = RedisSetCollection(
140
- redis_client,
141
- set_name=self.config.redis_port_key,
142
- )
143
135
  self.pool_queue = RedisQueue(
144
136
  redis_client,
145
137
  self.config.redis_container_pool_key,
146
138
  )
147
139
  else:
148
140
  self.container_mapping = InMemoryMapping()
149
- self.port_set = InMemorySetCollection()
150
141
  self.pool_queue = InMemoryQueue()
151
142
 
152
143
  self.container_deployment = self.config.container_deployment
153
144
 
154
145
  if base_url is None:
155
146
  if self.container_deployment == "docker":
156
- self.client = DockerClient()
147
+ self.client = DockerClient(config=self.config)
148
+ elif self.container_deployment == "k8s":
149
+ self.client = KubernetesClient(config=self.config)
157
150
  else:
158
- # TODO: support k8s deployment
159
151
  raise NotImplementedError("Not implemented")
160
152
  else:
161
153
  self.client = None
162
154
 
163
- self.port_range = range(*self.config.port_range)
164
-
165
155
  self.file_system = self.config.file_system
166
156
  if self.file_system == "oss":
167
157
  self.storage = OSSStorage(
@@ -235,34 +225,6 @@ class SandboxManager:
235
225
  logger.error(f"Error initializing runtime pool: {e}")
236
226
  break
237
227
 
238
- def _find_free_ports(self, n):
239
- free_ports = []
240
-
241
- for port in self.port_range:
242
- if len(free_ports) >= n:
243
- break # We have found enough ports
244
-
245
- if not self.port_set.add(port):
246
- continue
247
-
248
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
249
- try:
250
- s.bind(("", port))
251
- free_ports.append(port) # Port is available
252
-
253
- except OSError:
254
- # Bind failed, port is in use
255
- self.port_set.remove(port)
256
- # Try the next one
257
- continue
258
-
259
- if len(free_ports) < n:
260
- raise RuntimeError(
261
- "Not enough free ports available in the specified range.",
262
- )
263
-
264
- return free_ports
265
-
266
228
  @remote_wrapper()
267
229
  def cleanup(self):
268
230
  logger.debug(
@@ -414,7 +376,9 @@ class SandboxManager:
414
376
  )
415
377
  return None
416
378
 
417
- session_id = str(uuid4())
379
+ alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
380
+ short_uuid = shortuuid.ShortUUID(alphabet=alphabet).uuid()
381
+ session_id = str(short_uuid)
418
382
 
419
383
  if mount_dir is None:
420
384
  mount_dir = os.path.join(self.default_mount_dir, session_id)
@@ -439,12 +403,6 @@ class SandboxManager:
439
403
  f"Container with name {container_name} already exists.",
440
404
  )
441
405
 
442
- free_ports = self._find_free_ports(1)
443
-
444
- ports = {
445
- "80/tcp": free_ports[0], # nginx
446
- }
447
-
448
406
  # Generate a random secret token
449
407
  runtime_token = secrets.token_hex(16)
450
408
 
@@ -456,17 +414,19 @@ class SandboxManager:
456
414
  },
457
415
  }
458
416
 
459
- if not self.client.create(
417
+ _id, ports = self.client.create(
460
418
  image,
461
419
  name=container_name,
462
- ports=ports,
420
+ ports=["80/tcp"], # Nginx
463
421
  volumes=volume_bindings,
464
422
  environment={
465
423
  "SECRET_TOKEN": runtime_token,
466
424
  **environment,
467
425
  },
468
426
  runtime_config=config.runtime_config,
469
- ):
427
+ )
428
+
429
+ if _id is None:
470
430
  return None
471
431
 
472
432
  # Check the container status
@@ -478,24 +438,22 @@ class SandboxManager:
478
438
  )
479
439
  return None
480
440
 
481
- # Build the ContainerModel
482
- container_attrs = self.client.inspect(container_name)
483
-
441
+ # TODO: update ContainerModel according to images & backend
484
442
  container_model = ContainerModel(
485
443
  session_id=session_id,
486
- container_id=container_attrs["Id"], # Docker id pattern
444
+ container_id=_id,
487
445
  container_name=container_name,
488
- base_url=f"http://localhost:{ports['80/tcp']}/fastapi",
489
- browser_url=f"http://localhost:{ports['80/tcp']}/steel-api"
446
+ base_url=f"http://localhost:{ports[0]}/fastapi",
447
+ browser_url=f"http://localhost:{ports[0]}/steel-api"
490
448
  f"/{runtime_token}",
491
449
  front_browser_ws=f"ws://localhost:"
492
- f"{ports['80/tcp']}/steel-api/"
450
+ f"{ports[0]}/steel-api/"
493
451
  f"{runtime_token}/v1/sessions/cast",
494
452
  client_browser_ws=f"ws://localhost:"
495
- f"{ports['80/tcp']}/steel-api/{runtime_token}/&sessionId"
453
+ f"{ports[0]}/steel-api/{runtime_token}/&sessionId"
496
454
  f"={BROWSER_SESSION_ID}",
497
- artifacts_sio=f"http://localhost" f":{ports['80/tcp']}/v1",
498
- ports=free_ports,
455
+ artifacts_sio=f"http://localhost:{ports[0]}/v1",
456
+ ports=[ports[0]],
499
457
  mount_dir=str(mount_dir),
500
458
  storage_path=storage_path,
501
459
  runtime_token=runtime_token,
@@ -537,10 +495,6 @@ class SandboxManager:
537
495
  self.client.stop(container_info.container_id, timeout=1)
538
496
  self.client.remove(container_info.container_id, force=True)
539
497
 
540
- # Release ports after the container is removed
541
- for port in container_info.ports:
542
- self.port_set.remove(port)
543
-
544
498
  logger.debug(f"Container for {identity} destroyed.")
545
499
 
546
500
  # Upload to storage
@@ -570,14 +524,6 @@ class SandboxManager:
570
524
 
571
525
  container_info = ContainerModel(**container_json)
572
526
 
573
- # Check whether the ports are occupied by other processes
574
- for port in container_info.ports:
575
- if is_port_available(port):
576
- continue
577
-
578
- # If the port is occupied, sweep it
579
- sweep_port(port)
580
-
581
527
  self.client.start(container_info.container_id)
582
528
  status = self.client.get_status(container_info.container_id)
583
529
  if status != "running":
@@ -685,10 +631,3 @@ class SandboxManager:
685
631
  server_configs=server_configs,
686
632
  overwrite=overwrite,
687
633
  )
688
-
689
-
690
- if __name__ == "__main__":
691
- with SandboxManager() as manager:
692
- name = manager.create("12345")
693
- if name:
694
- print(f"Created container: {name}")
@@ -4,12 +4,14 @@ import inspect
4
4
  import logging
5
5
  import traceback
6
6
 
7
+ from typing import Optional
8
+
7
9
  from fastapi import FastAPI, HTTPException, Request, Depends
8
10
  from fastapi.middleware.cors import CORSMiddleware
9
11
  from fastapi.responses import JSONResponse
10
12
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
13
 
12
- from ...manager.server.config import settings
14
+ from ...manager.server.config import get_settings
13
15
  from ...manager.server.models import (
14
16
  ErrorResponse,
15
17
  HealthResponse,
@@ -42,34 +44,47 @@ app.add_middleware(
42
44
  security = HTTPBearer(auto_error=False)
43
45
 
44
46
  # Global SandboxManager instance
45
- _runtime_manager = None
46
- _config = SandboxManagerEnvConfig(
47
- container_prefix_key=settings.CONTAINER_PREFIX_KEY,
48
- file_system=settings.FILE_SYSTEM,
49
- redis_enabled=settings.REDIS_ENABLED,
50
- container_deployment=settings.CONTAINER_DEPLOYMENT,
51
- default_mount_dir=settings.DEFAULT_MOUNT_DIR,
52
- storage_folder=settings.STORAGE_FOLDER,
53
- port_range=settings.PORT_RANGE,
54
- pool_size=settings.POOL_SIZE,
55
- oss_endpoint=settings.OSS_ENDPOINT,
56
- oss_access_key_id=settings.OSS_ACCESS_KEY_ID,
57
- oss_access_key_secret=settings.OSS_ACCESS_KEY_SECRET,
58
- oss_bucket_name=settings.OSS_BUCKET_NAME,
59
- redis_server=settings.REDIS_SERVER,
60
- redis_port=settings.REDIS_PORT,
61
- redis_db=settings.REDIS_DB,
62
- redis_user=settings.REDIS_USER,
63
- redis_password=settings.REDIS_PASSWORD,
64
- redis_port_key=settings.REDIS_PORT_KEY,
65
- redis_container_pool_key=settings.REDIS_CONTAINER_POOL_KEY,
66
- )
47
+ _runtime_manager: Optional[SandboxManager] = None
48
+ _config: Optional[SandboxManagerEnvConfig] = None
49
+
50
+
51
+ def get_config() -> SandboxManagerEnvConfig:
52
+ """Return config"""
53
+ global _config
54
+ if _config is None:
55
+ settings = get_settings()
56
+ _config = SandboxManagerEnvConfig(
57
+ container_prefix_key=settings.CONTAINER_PREFIX_KEY,
58
+ file_system=settings.FILE_SYSTEM,
59
+ redis_enabled=settings.REDIS_ENABLED,
60
+ container_deployment=settings.CONTAINER_DEPLOYMENT,
61
+ default_mount_dir=settings.DEFAULT_MOUNT_DIR,
62
+ storage_folder=settings.STORAGE_FOLDER,
63
+ port_range=settings.PORT_RANGE,
64
+ pool_size=settings.POOL_SIZE,
65
+ oss_endpoint=settings.OSS_ENDPOINT,
66
+ oss_access_key_id=settings.OSS_ACCESS_KEY_ID,
67
+ oss_access_key_secret=settings.OSS_ACCESS_KEY_SECRET,
68
+ oss_bucket_name=settings.OSS_BUCKET_NAME,
69
+ redis_server=settings.REDIS_SERVER,
70
+ redis_port=settings.REDIS_PORT,
71
+ redis_db=settings.REDIS_DB,
72
+ redis_user=settings.REDIS_USER,
73
+ redis_password=settings.REDIS_PASSWORD,
74
+ redis_port_key=settings.REDIS_PORT_KEY,
75
+ redis_container_pool_key=settings.REDIS_CONTAINER_POOL_KEY,
76
+ k8s_namespace=settings.K8S_NAMESPACE,
77
+ kubeconfig_path=settings.KUBECONFIG_PATH,
78
+ )
79
+ return _config
67
80
 
68
81
 
69
82
  def verify_token(
70
83
  credentials: HTTPAuthorizationCredentials = Depends(security),
71
84
  ):
72
85
  """Verify Bearer token"""
86
+ settings = get_settings()
87
+
73
88
  if not hasattr(settings, "BEARER_TOKEN") or not settings.BEARER_TOKEN:
74
89
  logger.warning("BEARER_TOKEN not configured, skipping authentication")
75
90
  return credentials
@@ -98,8 +113,10 @@ def get_runtime_manager():
98
113
  """Get or create the global SandboxManager instance"""
99
114
  global _runtime_manager
100
115
  if _runtime_manager is None:
116
+ settings = get_settings()
117
+ config = get_config()
101
118
  _runtime_manager = SandboxManager(
102
- config=_config,
119
+ config=config,
103
120
  default_type=settings.DEFAULT_SANDBOX_TYPE,
104
121
  )
105
122
  return _runtime_manager
@@ -159,6 +176,7 @@ async def startup_event():
159
176
  async def shutdown_event():
160
177
  """Cleanup resources on shutdown"""
161
178
  global _runtime_manager
179
+ settings = get_settings()
162
180
  if _runtime_manager and settings.AUTO_CLEANUP:
163
181
  _runtime_manager.cleanup()
164
182
  _runtime_manager = None
@@ -179,8 +197,21 @@ async def health_check():
179
197
 
180
198
  def main():
181
199
  """Main entry point for the Runtime Manager Service"""
200
+ import argparse
201
+ import os
182
202
  import uvicorn
183
203
 
204
+ parser = argparse.ArgumentParser(description="Runtime Manager Service")
205
+ parser.add_argument("--config", type=str, help="Path to config file")
206
+ args = parser.parse_args()
207
+
208
+ if args.config and not os.path.exists(args.config):
209
+ raise FileNotFoundError(
210
+ f"Error: Config file {args.config} does not exist",
211
+ )
212
+
213
+ settings = get_settings(args.config)
214
+
184
215
  uvicorn.run(
185
216
  "agentscope_runtime.sandbox.manager.server.app:app",
186
217
  host=settings.HOST,
@@ -2,18 +2,9 @@
2
2
  import os
3
3
  from typing import Optional, Tuple, Literal
4
4
  from pydantic_settings import BaseSettings
5
- from pydantic import field_validator
5
+ from pydantic import field_validator, ConfigDict
6
6
  from dotenv import load_dotenv
7
7
 
8
- env_file = ".env"
9
- env_example_file = ".env.example"
10
-
11
- # Load the appropriate .env file
12
- if os.path.exists(env_file):
13
- load_dotenv(env_file)
14
- elif os.path.exists(env_example_file):
15
- load_dotenv(env_example_file)
16
-
17
8
 
18
9
  class Settings(BaseSettings):
19
10
  """Runtime Manager Service Settings"""
@@ -27,11 +18,10 @@ class Settings(BaseSettings):
27
18
 
28
19
  # Runtime Manager settings
29
20
  DEFAULT_SANDBOX_TYPE: str = "base"
30
- WORKDIR: str = "/workspace"
31
21
  POOL_SIZE: int = 1
32
22
  AUTO_CLEANUP: bool = True
33
23
  CONTAINER_PREFIX_KEY: str = "runtime_sandbox_container_"
34
- CONTAINER_DEPLOYMENT: Literal["docker", "cloud"] = "docker"
24
+ CONTAINER_DEPLOYMENT: Literal["docker", "cloud", "k8s"] = "docker"
35
25
  DEFAULT_MOUNT_DIR: str = "sessions_mount_dir"
36
26
  STORAGE_FOLDER: str = "runtime_sandbox_storage"
37
27
  PORT_RANGE: Tuple[int, int] = (49152, 59152)
@@ -53,9 +43,14 @@ class Settings(BaseSettings):
53
43
  OSS_ACCESS_KEY_SECRET: str = "your-access-key-secret"
54
44
  OSS_BUCKET_NAME: str = "your-bucket-name"
55
45
 
56
- class Config:
57
- env_file = env_file if os.path.exists(env_file) else env_example_file
58
- case_sensitive = True
46
+ # K8S settings
47
+ K8S_NAMESPACE: str = "default"
48
+ KUBECONFIG_PATH: Optional[str] = None
49
+
50
+ model_config = ConfigDict(
51
+ case_sensitive=True,
52
+ extra="allow",
53
+ )
59
54
 
60
55
  @field_validator("WORKERS", mode="before")
61
56
  @classmethod
@@ -65,4 +60,21 @@ class Settings(BaseSettings):
65
60
  return value
66
61
 
67
62
 
68
- settings = Settings()
63
+ _settings: Optional[Settings] = None
64
+
65
+
66
+ def get_settings(config_file: Optional[str] = None) -> Settings:
67
+ global _settings
68
+
69
+ env_file = ".env"
70
+ env_example_file = ".env.example"
71
+
72
+ if _settings is None:
73
+ if config_file and os.path.exists(config_file):
74
+ load_dotenv(config_file, override=True)
75
+ elif os.path.exists(env_file):
76
+ load_dotenv(env_file)
77
+ elif os.path.exists(env_example_file):
78
+ load_dotenv(env_example_file)
79
+ _settings = Settings()
80
+ return _settings
@@ -4,7 +4,6 @@ from typing import List
4
4
  from pydantic import BaseModel, Field
5
5
 
6
6
 
7
- # TODO: support k8s version
8
7
  class ContainerModel(BaseModel):
9
8
  session_id: str = Field(
10
9
  ...,
@@ -70,3 +69,6 @@ class ContainerModel(BaseModel):
70
69
  None,
71
70
  description="Image version of the container",
72
71
  )
72
+
73
+ class Config:
74
+ extra = "allow"
@@ -5,10 +5,14 @@ from typing import Optional, Literal, Tuple
5
5
  from pydantic import BaseModel, Field, model_validator
6
6
 
7
7
 
8
+ UUID_LENGTH = 25
9
+
10
+
8
11
  class SandboxManagerEnvConfig(BaseModel):
9
12
  container_prefix_key: str = Field(
10
13
  "runtime_sandbox_container_",
11
14
  description="Prefix for keys related to Container models.",
15
+ max_length=63 - UUID_LENGTH, # Max length for k8s pod name
12
16
  )
13
17
 
14
18
  file_system: Literal["local", "oss"] = Field(
@@ -23,9 +27,10 @@ class SandboxManagerEnvConfig(BaseModel):
23
27
  ...,
24
28
  description="Indicates if Redis is enabled.",
25
29
  )
26
- container_deployment: Literal["docker", "cloud"] = Field(
30
+ container_deployment: Literal["docker", "cloud", "k8s"] = Field(
27
31
  ...,
28
- description="container_deployment: 'docker'.",
32
+ description="Container deployment backend: 'docker', 'cloud', "
33
+ "or 'k8s'.",
29
34
  )
30
35
 
31
36
  default_mount_dir: Optional[str] = Field(
@@ -95,6 +100,18 @@ class SandboxManagerEnvConfig(BaseModel):
95
100
  description="Prefix for Redis keys related to container pool.",
96
101
  )
97
102
 
103
+ # Kubernetes settings
104
+ k8s_namespace: Optional[str] = Field(
105
+ "default",
106
+ description="Kubernetes namespace to deploy pods. Required if "
107
+ "container_deployment is 'k8s'.",
108
+ )
109
+ kubeconfig_path: Optional[str] = Field(
110
+ None,
111
+ description="Path to kubeconfig file. If not set, will try "
112
+ "in-cluster config or default kubeconfig.",
113
+ )
114
+
98
115
  @model_validator(mode="after")
99
116
  def check_settings(cls, self):
100
117
  if not self.default_mount_dir:
@@ -1,5 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # pylint: disable=unused-argument
3
+ import inspect
4
+
3
5
  from abc import ABC, abstractmethod
4
6
  from typing import Optional, Any, Dict
5
7
  from ..enums import SandboxType
@@ -121,3 +123,112 @@ class Tool(ABC):
121
123
  f"sandbox_type='{self.sandbox_type}'"
122
124
  f")"
123
125
  )
126
+
127
+ def make_function(self):
128
+ """Create a function with proper type signatures from schema."""
129
+ tool_call = self.__call__
130
+ parameters = self.schema["function"]["parameters"]
131
+
132
+ # Extract properties and required parameters from the schema
133
+ properties = parameters.get("properties", {})
134
+ required = parameters.get("required", [])
135
+
136
+ # Type mapping from JSON schema types to Python types
137
+ type_mapping = {
138
+ "string": str,
139
+ "integer": int,
140
+ "number": float,
141
+ "boolean": bool,
142
+ "array": list,
143
+ "object": dict,
144
+ }
145
+
146
+ # Build parameter signature
147
+ sig_params = []
148
+ for param_name, param_info in properties.items():
149
+ param_type = type_mapping.get(
150
+ param_info.get("type", "string"),
151
+ str,
152
+ )
153
+
154
+ if param_name in required:
155
+ # Required parameter
156
+ param = inspect.Parameter(
157
+ param_name,
158
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
159
+ annotation=param_type,
160
+ )
161
+ else:
162
+ # Optional parameter with default None
163
+ param = inspect.Parameter(
164
+ param_name,
165
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
166
+ default=None,
167
+ annotation=Optional[param_type],
168
+ )
169
+
170
+ sig_params.append(param)
171
+
172
+ # Create the function signature
173
+ new_signature = inspect.Signature(sig_params, return_annotation=Any)
174
+
175
+ def generated_function(*args, **kwargs):
176
+ """
177
+ Dynamically generated function wrapper for the tool schema.
178
+
179
+ This function is created at runtime to match the tool's parameter
180
+ signature as defined in the schema. It validates arguments and
181
+ forwards them to the tool's call interface.
182
+ """
183
+ # Bind arguments to signature
184
+ bound = new_signature.bind(*args, **kwargs)
185
+ bound.apply_defaults()
186
+
187
+ # Validate required parameters
188
+ missing_required = [
189
+ param_name
190
+ for param_name in required
191
+ if param_name not in bound.arguments
192
+ or bound.arguments[param_name] is None
193
+ ]
194
+
195
+ if missing_required:
196
+ raise TypeError(
197
+ f"Missing required arguments: {set(missing_required)}",
198
+ )
199
+
200
+ # Filter kwargs based on defined properties and remove None
201
+ # values for optional params
202
+ filtered_kwargs = {
203
+ k: v
204
+ for k, v in bound.arguments.items()
205
+ if k in properties and (k in required or v is not None)
206
+ }
207
+
208
+ return tool_call(**filtered_kwargs)
209
+
210
+ # Set the correct signature and metadata
211
+ generated_function.__signature__ = new_signature
212
+ generated_function.__name__ = self.name
213
+
214
+ # Build docstring with parameter information
215
+ doc_parts = []
216
+ for name, info in properties.items():
217
+ required_str = " (required)" if name in required else " (optional)"
218
+ doc_parts.append(
219
+ f" {name}: {info.get('type', 'string')}{required_str} -"
220
+ f" {info.get('description', '')}",
221
+ )
222
+
223
+ generated_function.__doc__ = (
224
+ self.schema["function"]["description"]
225
+ + "\n\nParameters:\n"
226
+ + "\n".join(doc_parts)
227
+ )
228
+
229
+ # Set type annotations for compatibility with typing inspection
230
+ annotations = {param.name: param.annotation for param in sig_params}
231
+ annotations["return"] = Any
232
+ generated_function.__annotations__ = annotations
233
+
234
+ return generated_function
@@ -1,2 +1,2 @@
1
1
  # -*- coding: utf-8 -*-
2
- __version__ = "v0.0.1"
2
+ __version__ = "v0.1.1"