clap-agents 0.1.1__py3-none-any.whl → 0.2.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.
clap/tool_pattern/tool.py CHANGED
@@ -1,229 +1,158 @@
1
-
2
1
  import json
3
2
  import inspect
4
- import functools # Import functools
5
- from typing import Callable, Any
3
+ import functools
4
+ from typing import Callable, Any, Dict, List
6
5
  import anyio
7
- import jsonschema
6
+ import jsonschema
8
7
 
9
8
 
10
9
  def get_fn_signature(fn: Callable) -> dict:
11
- """
12
- Generates the signature (schema) for a given function in JSON Schema format.
13
-
14
- Args:
15
- fn (Callable): The function whose signature needs to be extracted.
16
-
17
- Returns:
18
- dict: A dictionary representing the function's schema.
19
- """
20
-
10
+
21
11
  type_mapping = {
22
- "int": "integer",
23
- "str": "string",
24
- "bool": "boolean",
25
- "float": "number",
26
- "list": "array", # Basic support for lists
27
- "dict": "object", # Basic support for dicts
12
+ "int": "integer", "str": "string", "bool": "boolean",
13
+ "float": "number", "list": "array", "dict": "object",
28
14
  }
29
-
30
15
  parameters = {"type": "object", "properties": {}, "required": []}
31
16
  sig = inspect.signature(fn)
32
-
33
17
  for name, type_hint in fn.__annotations__.items():
34
- if name == "return":
35
- continue
18
+ if name == "return": continue
36
19
  param_type_name = getattr(type_hint, "__name__", str(type_hint))
37
- schema_type = type_mapping.get(param_type_name, "string")
38
-
20
+ schema_type = type_mapping.get(param_type_name.lower(), "string")
39
21
  parameters["properties"][name] = {"type": schema_type}
40
-
41
22
  if sig.parameters[name].default is inspect.Parameter.empty:
42
23
  parameters["required"].append(name)
43
-
44
- if not parameters["required"]:
45
- del parameters["required"]
46
-
47
- fn_schema: dict = {
48
- "type": "function",
49
- "function": {
50
- "name": fn.__name__,
51
- "description": fn.__doc__,
52
- "parameters": parameters,
53
- }
54
- }
55
-
56
- return fn_schema
24
+ if not parameters.get("required"):
25
+ if "required" in parameters: del parameters["required"]
26
+ return {"type": "function", "function": {"name": fn.__name__, "description": fn.__doc__, "parameters": parameters}}
57
27
 
58
28
 
59
- def validate_arguments(tool_call_args: dict, tool_schema: dict) -> dict:
29
+ def validate_and_coerce_arguments(tool_call_args: Dict[str, Any], tool_schema: Dict[str, Any]) -> Dict[str, Any]: # Renamed for clarity
60
30
  """
61
- Validates and converts arguments in the input dictionary based on the tool's JSON schema.
62
- NOTE: This is a simplified validator. For production, use a robust JSON Schema validator.
31
+ Validates and coerces arguments in the input dictionary based on the tool's JSON schema.
32
+ Then, performs final validation using jsonschema.
63
33
 
64
34
  Args:
65
- tool_call_args (dict): The arguments provided for the tool call (usually strings from LLM).
66
- tool_schema (dict): The JSON schema for the tool's parameters.
35
+ tool_call_args (Dict[str, Any]): Arguments from LLM (often strings).
36
+ tool_schema (Dict[str, Any]): Tool's JSON schema.
67
37
 
68
38
  Returns:
69
- dict: The arguments dictionary with values converted to the correct types if possible.
39
+ Dict[str, Any]: Arguments with values coerced to correct types.
70
40
 
71
41
  Raises:
72
- ValueError: If conversion fails for a required argument.
42
+ jsonschema.ValidationError: If final validation fails after coercion.
43
+ ValueError: If coercion fails for a required argument or types are incompatible.
73
44
  """
74
- properties = tool_schema.get("function", {}).get("parameters", {}).get("properties", {})
75
- validated_args = {}
45
+ parameter_schema = tool_schema.get("function", {}).get("parameters", {})
46
+ properties = parameter_schema.get("properties", {})
47
+ required_args = parameter_schema.get("required", [])
48
+
49
+ coerced_args: Dict[str, Any] = {}
76
50
 
77
- type_mapping = {
51
+
52
+ string_coercion_map = {
78
53
  "integer": int,
54
+ "number": float,
55
+ "boolean": lambda x: x.lower() in ['true', '1', 'yes'] if isinstance(x, str) else bool(x),
79
56
  "string": str,
80
- "boolean": bool,
81
- "number": float,
82
- "array": list,
83
- "object": dict
57
+
58
+ "array": lambda x: json.loads(x) if isinstance(x, str) else x,
59
+ "object": lambda x: json.loads(x) if isinstance(x, str) else x,
60
+ }
61
+
62
+ expected_python_type_map = {
63
+ "integer": int, "number": (int, float), "boolean": bool,
64
+ "string": str, "array": list, "object": dict,
84
65
  }
85
66
 
86
67
  for arg_name, arg_value in tool_call_args.items():
87
68
  prop_schema = properties.get(arg_name)
88
69
  if not prop_schema:
89
- # Argument not defined in schema, potentially skip or warn
90
- print(f"Warning: Argument '{arg_name}' not found in tool schema.")
91
- validated_args[arg_name] = arg_value # Pass through unknown args for now
70
+
71
+ coerced_args[arg_name] = arg_value
92
72
  continue
93
73
 
94
- expected_type_name = prop_schema.get("type")
95
- expected_type = type_mapping.get(expected_type_name)
74
+ expected_schema_type = prop_schema.get("type")
75
+ python_type_tuple = expected_python_type_map.get(expected_schema_type)
76
+
77
+
78
+ if python_type_tuple and isinstance(arg_value, python_type_tuple):
79
+ coerced_args[arg_name] = arg_value
80
+ continue
96
81
 
97
- if expected_type:
82
+
83
+ coercer = string_coercion_map.get(expected_schema_type)
84
+ if coercer:
98
85
  try:
99
- if not isinstance(arg_value, expected_type):
100
- if expected_type is bool and isinstance(arg_value, str):
101
- if arg_value.lower() in ['true', '1', 'yes']:
102
- validated_args[arg_name] = True
103
- elif arg_value.lower() in ['false', '0', 'no']:
104
- validated_args[arg_name] = False
105
- else:
106
- raise ValueError(f"Cannot convert string '{arg_value}' to boolean.")
107
- # Basic handling for array/object assuming JSON string
108
- elif expected_type in [list, dict] and isinstance(arg_value, str):
109
- try:
110
- validated_args[arg_name] = json.loads(arg_value)
111
- if not isinstance(validated_args[arg_name], expected_type):
112
- raise ValueError(f"Decoded JSON for '{arg_name}' is not the expected type '{expected_type_name}'.")
113
- except json.JSONDecodeError:
114
- raise ValueError(f"Argument '{arg_name}' with value '{arg_value}' is not valid JSON for type '{expected_type_name}'.")
115
- else:
116
- validated_args[arg_name] = expected_type(arg_value)
86
+ coerced_value = coercer(arg_value)
87
+
88
+ if expected_schema_type in ["array", "object"] and python_type_tuple:
89
+ if not isinstance(coerced_value, python_type_tuple):
90
+ raise ValueError(f"Decoded JSON for '{arg_name}' is not expected type '{expected_schema_type}'.")
91
+ coerced_args[arg_name] = coerced_value
92
+ except (ValueError, TypeError, json.JSONDecodeError) as e:
93
+
94
+
95
+ if arg_name in required_args:
96
+ raise ValueError(
97
+ f"Error coercing required argument '{arg_name}' with value '{arg_value}' "
98
+ f"to type '{expected_schema_type}': {e}"
99
+ )
117
100
  else:
118
- # Type is already correct
119
- validated_args[arg_name] = arg_value
120
- except (ValueError, TypeError) as e:
121
- raise ValueError(f"Error converting argument '{arg_name}' with value '{arg_value}' to type '{expected_type_name}': {e}")
101
+ coerced_args[arg_name] = arg_value
122
102
  else:
123
- # Unknown type in schema, pass through
124
- validated_args[arg_name] = arg_value
103
+
104
+ coerced_args[arg_name] = arg_value
125
105
 
126
- # Check for missing required arguments (optional, depends on strictness)
127
- # required_args = tool_schema.get("function", {}).get("parameters", {}).get("required", [])
128
- # for req_arg in required_args:
129
- # if req_arg not in validated_args:
130
- # raise ValueError(f"Missing required argument: '{req_arg}'")
106
+
107
+ try:
108
+ jsonschema.validate(instance=coerced_args, schema=parameter_schema)
109
+ except jsonschema.ValidationError as e:
110
+
111
+ raise
131
112
 
113
+ return coerced_args
132
114
 
133
- return validated_args
134
115
 
135
116
  class Tool:
136
- """
137
- A class representing a tool that wraps a callable and its schema.
138
- Handles both synchronous and asynchronous functions.
139
-
140
- Attributes:
141
- name (str): The name of the tool (function).
142
- fn (Callable): The function that the tool represents (can be sync or async).
143
- fn_schema (dict): Dictionary representing the function's schema in JSON Schema format.
144
- fn_signature (str): JSON string representation of the function's signature (legacy, kept for potential compatibility).
145
- """
146
-
147
117
  def __init__(self, name: str, fn: Callable, fn_schema: dict):
148
118
  self.name = name
149
119
  self.fn = fn
150
120
  self.fn_schema = fn_schema
151
- self.fn_signature = json.dumps(fn_schema)
121
+ # self.fn_signature = json.dumps(fn_schema)
152
122
 
153
123
  def __str__(self):
154
124
  return json.dumps(self.fn_schema, indent=2)
155
125
 
156
- async def run(self, **kwargs) -> Any:
126
+ async def run(self, **kwargs: Any) -> Any:
157
127
  """
158
- Executes the tool (function) with provided arguments asynchronously.
159
- Validates arguments against the tool's JSON schema before execution.
160
- Handles both sync and async tool functions appropriately.
161
-
162
- Args:
163
- **kwargs: Keyword arguments provided for the tool call.
164
-
165
- Returns:
166
- The result of the function call, or an error string.
128
+ Executes the tool, validating and coercing arguments first.
167
129
  """
168
- parameter_schema = self.fn_schema.get("function", {}).get("parameters", {})
169
-
170
- # --- Use jsonschema for validation ---
130
+ function_name = self.fn_schema.get("function", {}).get("name", "unknown_tool")
171
131
  try:
172
- # Validate the incoming arguments against the parameter schema
173
- # Note: jsonschema validates, it doesn't coerce types like the old function
174
- jsonschema.validate(instance=kwargs, schema=parameter_schema)
175
- # If validation passes, kwargs are structurally correct according to schema
176
-
177
- # Type Coercion/Conversion might still be needed depending on self.fn
178
- # If self.fn uses Pydantic models or type hints, it might handle coercion.
179
- # Or, you could apply specific conversions based on schema after validation if needed.
180
- # For now, assume self.fn or Pydantic handles coercion post-validation.
181
- validated_kwargs = kwargs # Use original kwargs after validation passes
182
-
183
- except jsonschema.ValidationError as e:
184
- print(f"Argument validation failed for tool {self.name}: {e.message}")
185
- return f"Error: Invalid arguments provided - {e.message}"
186
- except Exception as e: # Catch other potential validation setup errors
187
- print(f"An unexpected error occurred during argument validation for tool {self.name}: {e}")
188
- return f"Error: Argument validation failed."
189
- # --- End jsonschema validation ---
190
-
191
- # --- Execute the function (sync or async) ---
132
+
133
+ validated_and_coerced_kwargs = validate_and_coerce_arguments(kwargs, self.fn_schema)
134
+ except (jsonschema.ValidationError, ValueError) as e:
135
+
136
+ return f"Error: Invalid arguments for tool {function_name} - {str(e)}"
137
+ except Exception as e:
138
+ return f"Error: Unexpected issue with arguments for tool {function_name}."
139
+
140
+
192
141
  try:
193
142
  if inspect.iscoroutinefunction(self.fn):
194
- return await self.fn(**validated_kwargs)
143
+ return await self.fn(**validated_and_coerced_kwargs)
195
144
  else:
196
- func_with_args = functools.partial(self.fn, **validated_kwargs)
145
+
146
+ func_with_args = functools.partial(self.fn, **validated_and_coerced_kwargs)
197
147
  return await anyio.to_thread.run_sync(func_with_args)
198
148
  except Exception as e:
199
- # Catch errors during the actual tool execution
200
- print(f"Error executing tool {self.name}: {e}")
201
- # Consider logging traceback here
202
- return f"Error executing tool: {e}"
203
-
149
+
150
+ return f"Error executing tool {function_name}: {e}"
204
151
 
205
152
 
206
153
  def tool(fn: Callable):
207
- """
208
- A decorator that wraps a function (sync or async) into a Tool object,
209
- including its JSON schema.
210
-
211
- Args:
212
- fn (Callable): The function to be wrapped.
213
-
214
- Returns:
215
- Tool: A Tool object containing the function, its name, and its schema.
216
- """
217
-
218
- def wrapper():
219
- fn_schema = get_fn_signature(fn)
220
- if not fn_schema or 'function' not in fn_schema or 'name' not in fn_schema['function']:
221
- raise ValueError(f"Could not generate valid schema for function {fn.__name__}")
222
- return Tool(
223
- name=fn_schema["function"]["name"],
224
- fn=fn,
225
- fn_schema=fn_schema
226
- )
227
-
228
- return wrapper()
154
+ fn_schema = get_fn_signature(fn)
155
+ if not fn_schema or 'function' not in fn_schema or 'name' not in fn_schema['function']:
156
+ raise ValueError(f"Could not generate valid schema for function {fn.__name__}")
157
+ return Tool(name=fn_schema["function"]["name"], fn=fn, fn_schema=fn_schema)
229
158