Skip to content
This repository was archived by the owner on Apr 29, 2025. It is now read-only.

Commit 6eec66f

Browse files
committed
v0.0.17
1 parent e7820ed commit 6eec66f

File tree

9 files changed

+154
-61
lines changed

9 files changed

+154
-61
lines changed

CHANGELOG.md

+20-9
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,42 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/),
66
and this project adheres to [SimpleTool](https://github.com/nchekwa/simpletool-python/tree/master).
77

8-
# [0.0.16] - 2025-01-11 Milestone Alpha2
8+
## [0.0.17] - 2025-01-13 Milestone Alpha2
99

10-
## Fixed
10+
### Added
11+
- add `name` and `description` validation Field rules in `SimpleToolResponseModel`
12+
- add str to `ResourceConent`-*uri* field to match rfc3986 uris
13+
14+
### Changed
15+
- `ImageContent` - rename *data* -> *image*
16+
- `FileContent` - rename *data* -> *file*
17+
- `ErrorContent` - rename *message* -> *error*
18+
19+
## [0.0.16] - 2025-01-11 Milestone Alpha2
20+
21+
### Fixed
1122
- incorrect `SimpleToolResponseModel` Pydantic model configuration + from_attributes allow easy serialize/deserialize
1223

13-
# [0.0.15] - 2025-01-09 Milestone Alpha2
24+
## [0.0.15] - 2025-01-09 Milestone Alpha2
1425

15-
## Fixed
26+
### Fixed
1627
- incorrect github action workflow for `setup.py` version
1728

18-
# [0.0.14] - 2025-01-09 Milestone Alpha2
29+
## [0.0.14] - 2025-01-09 Milestone Alpha2
1930

20-
## Added
31+
### Added
2132
- SimpleToolResponseModel
2233
- add correct handle __repr__ for SimpleTool child classes
2334
- add get_version for `setup.py` to automate version update
2435
- auto add annotation version in `__init__.py`
2536

26-
# [0.0.13] - 2025-01-08 Milestone Alpha2
37+
## [0.0.13] - 2025-01-08 Milestone Alpha2
2738

28-
## Fixed
39+
### Fixed
2940
- Updated test import from `BoolContents` to `BoolContent` to match type definition
3041

3142

32-
# [0.0.12] - 2025-01-08 Milestone Alpha2
43+
## [0.0.12] - 2025-01-08 Milestone Alpha2
3344

3445
### Added
3546
- added `input_model` will be mandatory - as mapping SimpleInputModel to SimpleTool

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
pydantic>=2.10.4
1+
pydantic>=2.10.4
2+
rfc3986

setup.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ def get_version():
99
with open('CHANGELOG.md', 'r', encoding='utf-8') as f:
1010
for line in f:
1111
line = line.strip()
12-
match = re.search(r'# \[(\d+\.\d+\.\d+)\]', line)
12+
# Modified pattern to match both # [...] and ## [...]
13+
match = re.search(r'^#{1,2}\s*\[(\d+\.\d+\.\d+)\]', line)
1314
if match:
1415
version = match.group(1)
1516

@@ -40,6 +41,7 @@ def get_version():
4041
print("CHANGELOG.md not found!")
4142
return '0.0.0'
4243

44+
4345
def read_version_from_init():
4446
"""Read version from __init__.py as a fallback"""
4547
try:
@@ -55,25 +57,27 @@ def read_version_from_init():
5557
print(f"Error reading version from __init__.py: {e}")
5658
return '0.0.0'
5759

60+
5861
def write_version_to_metadata(version):
5962
"""Write version to PKG-INFO metadata file"""
6063
try:
6164
with open('simpletool.egg-info/PKG-INFO', 'r') as f:
6265
content = f.read()
63-
66+
6467
# Replace or add Version
6568
if 'Version:' in content:
6669
content = re.sub(r'Version:.*', f'Version: {version}', content)
6770
else:
6871
content += f'\nVersion: {version}\n'
69-
72+
7073
with open('simpletool.egg-info/PKG-INFO', 'w') as f:
7174
f.write(content)
72-
75+
7376
print(f"Updated PKG-INFO with version: {version}")
7477
except Exception as e:
7578
print(f"Error writing version to metadata: {e}")
7679

80+
7781
# First try to get version from CHANGELOG.md
7882
version = get_version()
7983

@@ -92,7 +96,7 @@ def write_version_to_metadata(version):
9296
author_email='[email protected]',
9397
license='MIT',
9498
packages=['simpletool'],
95-
install_requires=['pydantic>=2.0.0', 'typing-extensions', 'pydantic>=2.10.4'],
99+
install_requires=['pydantic>=2.10.4', 'typing-extensions'],
96100
long_description=open('README.md').read(),
97101
long_description_content_type='text/markdown',
98102
package_data={

simpletool/__init__.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from functools import wraps
1313
from abc import ABC
1414
from typing import List, Dict, Any, Union, Type, Literal, Sequence, Tuple, get_args
15-
from typing import Optional, TypeVar, AnyStr, Callable, Awaitable, Coroutine, ClassVar # noqa: F401, F403
15+
from typing import Optional, TypeVar, AnyStr, Callable, Awaitable, Coroutine, ClassVar, TypeAlias # noqa: F401, F403
1616
from pydantic import BaseModel, Field
1717
from pydantic.fields import FieldInfo
1818
from .types import Content, TextContent, ImageContent, FileContent, ResourceContent, BoolContent, ErrorContent
@@ -50,7 +50,7 @@ async def wrapper(*args, **kwargs):
5050
except asyncio.TimeoutError:
5151
return [ErrorContent(
5252
code=408, # Request Timeout
53-
message=f"Tool execution timed out after {seconds} seconds",
53+
error=f"Tool execution timed out after {seconds} seconds",
5454
data={"timeout": seconds}
5555
)]
5656
return wrapper
@@ -238,7 +238,7 @@ async def run(self, arguments: Dict[str, Any]) -> Sequence[Union[Content, TextCo
238238
except ValidationError as e:
239239
return [ErrorContent(
240240
code=400, # Bad Request
241-
message=f"Input validation error: {str(e)}",
241+
error=f"Input validation error: {str(e)}",
242242
data={"validation_error": str(e)}
243243
)]
244244

@@ -254,7 +254,7 @@ async def run(self, arguments: Dict[str, Any]) -> Sequence[Union[Content, TextCo
254254
except asyncio.TimeoutError:
255255
return [ErrorContent(
256256
code=408, # Request Timeout
257-
message=f"Tool execution timed out after {self._timeout} seconds",
257+
error=f"Tool execution timed out after {self._timeout} seconds",
258258
data={"timeout": self._timeout}
259259
)]
260260
else:
@@ -338,7 +338,7 @@ def info(self) -> str:
338338

339339
@property
340340
def to_dict(self) -> Dict[str, Any]:
341-
"""Return a dictionary representation of the tool."""
341+
"""Convert content to a dictionary representation"""
342342
sorted_input_schema = self._sort_input_schema(self.input_schema)
343343
return {
344344
"name": self.name,

simpletool/models.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,17 @@ class SimpleToolResponseModel(BaseModel):
4040
description (str): A description of the tool's functionality.
4141
input_schema (Optional[dict]): The input schema for the tool, if available.
4242
"""
43-
name: str = Field(..., description="Name of the tool")
44-
description: str = Field(..., description="Description of the tool's functionality")
43+
name: str = Field(
44+
...,
45+
description="Name of the tool",
46+
pattern="^[a-zA-Z0-9_-]+$",
47+
max_length=64
48+
)
49+
description: str = Field(
50+
...,
51+
description="Description of the tool's functionality",
52+
max_length=1024
53+
)
4554
input_schema: Optional[dict] = Field(None, description="Input schema for the tool, if available")
4655

4756
class Config:

simpletool/types.py

+39-27
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
""" Type definitions for the simpletool package."""
2-
from typing import Literal, Any, Optional
2+
from typing import Literal, Any, Optional, Union
33
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
44
from pydantic.networks import AnyUrl
55
import base64
66

7+
78
# -------------------------------------------------------------------------------------------------
89
# --- CONTENT CLASSES -----------------------------------------------------------------------------
910
# -------------------------------------------------------------------------------------------------
@@ -16,12 +17,16 @@ class Content(BaseModel):
1617

1718
@model_validator(mode='before')
1819
@classmethod
19-
def _convert_camel_to_snake_names(cls, data):
20-
if isinstance(data, dict) and 'mimeType' in data:
21-
data['mime_type'] = data.pop('mimeType')
22-
if isinstance(data, dict) and 'fileName' in data:
23-
data['file_name'] = data.pop('fileName')
24-
return data
20+
def convert_field_names(cls, data: dict) -> dict:
21+
if not isinstance(data, dict):
22+
return data
23+
24+
field_mappings = {
25+
'mimeType': 'mime_type',
26+
'fileName': 'file_name'
27+
}
28+
29+
return {field_mappings.get(k, k): v for k, v in data.items()}
2530

2631

2732
class TextContent(Content):
@@ -41,66 +46,66 @@ def validate_or_convert(cls, data):
4146
class ImageContent(Content):
4247
"""Image content for a message."""
4348
type: Literal["image"] = "image" # type: ignore
44-
data: str
49+
image: str
4550
mime_type: str | None = None
4651
description: Optional[str] | None = None
4752

4853
@model_validator(mode='before')
4954
@classmethod
50-
def validate_or_convert(cls, data):
55+
def validate_or_convert(cls, image):
5156
# If a string is passed directly, assume it's base64 data
52-
if isinstance(data, str):
57+
if isinstance(image, str):
5358
# Validate base64 encoding
5459
try:
55-
base64.b64decode(data, validate=True)
56-
return {"data": data}
60+
base64.b64decode(image, validate=True)
61+
return {"image": image}
5762
except Exception as e:
58-
raise ValueError("Data must be a valid base64 encoded string") from e
59-
return data
63+
raise ValueError("Image must be a valid base64 encoded string") from e
64+
return image
6065

61-
@field_validator('data')
66+
@field_validator('image')
6267
@classmethod
6368
def validate_base64(cls, value):
6469
try:
6570
base64.b64decode(value, validate=True)
6671
return value
6772
except Exception as e:
68-
raise ValueError("Data must be a valid base64 encoded string") from e
73+
raise ValueError("Image must be a valid base64 encoded string") from e
6974

7075

7176
class FileContent(Content):
7277
type: Literal["file"] = "file" # type: ignore
73-
data: str
78+
file: str
7479
mime_type: str | None = None
7580
file_name: Optional[str] | None = None
7681
description: Optional[str] | None = None
7782

7883
@model_validator(mode='before')
7984
@classmethod
80-
def validate_or_convert(cls, data):
85+
def validate_or_convert(cls, file):
8186
# If a string is passed directly, assume it's base64 data
82-
if isinstance(data, str):
87+
if isinstance(file, str):
8388
# Validate base64 encoding
8489
try:
85-
base64.b64decode(data, validate=True)
86-
return {"data": data}
90+
base64.b64decode(file, validate=True)
91+
return {"file": file}
8792
except Exception as e:
88-
raise ValueError("Data must be a valid base64 encoded string") from e
89-
return data
93+
raise ValueError("File must be a valid base64 encoded string") from e
94+
return file
9095

91-
@field_validator('data')
96+
@field_validator('file')
9297
@classmethod
9398
def validate_base64(cls, value):
9499
try:
95100
base64.b64decode(value, validate=True)
96101
return value
97102
except Exception as e:
98-
raise ValueError("Data must be a valid base64 encoded string") from e
103+
raise ValueError("File must be a valid base64 encoded string") from e
99104

100105

101106
class ResourceContent(Content):
102107
type: Literal["resource"] = "resource" # type: ignore
103-
uri: AnyUrl
108+
uri: Union[str, AnyUrl]
104109
name: str
105110
description: Optional[str] | None = None
106111
mime_type: str | None = None
@@ -113,12 +118,19 @@ def validate_or_convert(cls, data):
113118
return {"uri": data, "name": str(data)}
114119
return data
115120

121+
@field_validator('uri')
122+
@classmethod
123+
def validate_uri(cls, v: str) -> str:
124+
"""Ensure URI is accessible and returns valid status"""
125+
# Add URI validation logic here
126+
return v
127+
116128

117129
class ErrorContent(Content):
118130
"""Error information for JSON-RPC error responses."""
119131
type: Literal["error"] = "error" # type: ignore
120132
code: int = Field(description="A number that indicates the error type that occurred.")
121-
message: str = Field(description="A short description of the error. The message SHOULD be limited to a concise single sentence.")
133+
error: str = Field(description="A short description of the error. The message SHOULD be limited to a concise single sentence.")
122134
data: Any | None = Field(default=None, description="Additional information about the error.")
123135
model_config = ConfigDict(extra="allow")
124136

tests/test_models.py

+14
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ def test_simple_input_model_camel_case_conversion():
5555
assert model.camel_case_field == "another test value"
5656

5757

58+
def test_simple_input_model_without_input_schema():
59+
"""Test conversion when inputSchema is not present."""
60+
data = {"someField": "test value"}
61+
result = SimpleInputModel._convert_camel_to_snake_names(data)
62+
assert result == {"someField": "test value"}
63+
assert "input_schema" not in result
64+
65+
def test_simple_input_model_with_input_schema():
66+
"""Test conversion when inputSchema is present."""
67+
data = {"inputSchema": {"type": "object"}}
68+
result = SimpleInputModel._convert_camel_to_snake_names(data)
69+
assert "inputSchema" not in result
70+
assert result["input_schema"] == {"type": "object"}
71+
5872
def test_simple_tool_model():
5973
"""Test SimpleToolModel initialization."""
6074
tool_model = SimpleToolModel(

tests/test_simpletool.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ async def fast_tool():
9393
assert len(result) == 1
9494
assert isinstance(result[0], ErrorContent)
9595
assert result[0].code == 408
96-
assert "timed out" in result[0].message
96+
assert "timed out" in result[0].error
9797

9898
# Test non-timeout
9999
result = asyncio.run(fast_tool())
@@ -232,7 +232,7 @@ async def run(self, arguments: Dict[str, Any]) -> Sequence[Union[TextContent, Er
232232
if not arguments.get('optional_field'):
233233
return [ErrorContent(
234234
code=400,
235-
message="Optional field is required",
235+
error="Optional field is required",
236236
data={"input": arguments}
237237
)]
238238
return [TextContent(type="text", text=str(arguments['optional_field']))]

0 commit comments

Comments
 (0)