cognitive-modules 0.6.0__tar.gz → 0.6.1__tar.gz
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.
- {cognitive_modules-0.6.0/src/cognitive_modules.egg-info → cognitive_modules-0.6.1}/PKG-INFO +1 -1
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/pyproject.toml +1 -1
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/runner.py +213 -20
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1/src/cognitive_modules.egg-info}/PKG-INFO +1 -1
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/LICENSE +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/README.md +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/setup.cfg +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/__init__.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/cli.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/loader.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/mcp_server.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/migrate.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/providers/__init__.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/registry.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/server.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/subagent.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/templates.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/validator.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/SOURCES.txt +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/dependency_links.txt +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/entry_points.txt +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/requires.txt +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/top_level.txt +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_cli.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_loader.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_migrate.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_registry.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_runner.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_subagent.py +0 -0
- {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_validator.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cognitive-modules"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "Structured LLM task runner with schema validation, confidence scoring, and subagent orchestration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -930,54 +930,247 @@ SUPPORTED_VIDEO_TYPES = {
|
|
|
930
930
|
"video/mp4", "video/webm", "video/quicktime"
|
|
931
931
|
}
|
|
932
932
|
|
|
933
|
+
# Magic bytes for media type detection
|
|
934
|
+
MEDIA_MAGIC_BYTES = {
|
|
935
|
+
"image/jpeg": [b"\xff\xd8\xff"],
|
|
936
|
+
"image/png": [b"\x89PNG\r\n\x1a\n"],
|
|
937
|
+
"image/gif": [b"GIF87a", b"GIF89a"],
|
|
938
|
+
"image/webp": [b"RIFF"], # Check WEBP signature later
|
|
939
|
+
"audio/mpeg": [b"\xff\xfb", b"\xff\xfa", b"ID3"],
|
|
940
|
+
"audio/wav": [b"RIFF"], # Check WAVE signature later
|
|
941
|
+
"audio/ogg": [b"OggS"],
|
|
942
|
+
"video/mp4": [b"\x00\x00\x00"], # ftyp check needed
|
|
943
|
+
"video/webm": [b"\x1a\x45\xdf\xa3"],
|
|
944
|
+
"application/pdf": [b"%PDF"],
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
# Media size limits in bytes
|
|
948
|
+
MEDIA_SIZE_LIMITS = {
|
|
949
|
+
"image": 20 * 1024 * 1024, # 20MB
|
|
950
|
+
"audio": 25 * 1024 * 1024, # 25MB
|
|
951
|
+
"video": 100 * 1024 * 1024, # 100MB
|
|
952
|
+
"document": 50 * 1024 * 1024, # 50MB
|
|
953
|
+
}
|
|
933
954
|
|
|
934
|
-
|
|
955
|
+
# Media dimension limits
|
|
956
|
+
MEDIA_DIMENSION_LIMITS = {
|
|
957
|
+
"max_width": 8192,
|
|
958
|
+
"max_height": 8192,
|
|
959
|
+
"min_width": 10,
|
|
960
|
+
"min_height": 10,
|
|
961
|
+
"max_pixels": 67108864, # 8192 x 8192
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
# v2.5 Error codes
|
|
965
|
+
ERROR_CODES_V25 = {
|
|
966
|
+
"UNSUPPORTED_MEDIA_TYPE": "E1010",
|
|
967
|
+
"MEDIA_TOO_LARGE": "E1011",
|
|
968
|
+
"MEDIA_FETCH_FAILED": "E1012",
|
|
969
|
+
"MEDIA_DECODE_FAILED": "E1013",
|
|
970
|
+
"MEDIA_TYPE_MISMATCH": "E1014",
|
|
971
|
+
"MEDIA_DIMENSION_EXCEEDED": "E1015",
|
|
972
|
+
"MEDIA_DIMENSION_TOO_SMALL": "E1016",
|
|
973
|
+
"MEDIA_PIXEL_LIMIT": "E1017",
|
|
974
|
+
"UPLOAD_EXPIRED": "E1018",
|
|
975
|
+
"UPLOAD_NOT_FOUND": "E1019",
|
|
976
|
+
"CHECKSUM_MISMATCH": "E1020",
|
|
977
|
+
"STREAM_INTERRUPTED": "E2010",
|
|
978
|
+
"STREAM_TIMEOUT": "E2011",
|
|
979
|
+
"STREAMING_NOT_SUPPORTED": "E4010",
|
|
980
|
+
"MULTIMODAL_NOT_SUPPORTED": "E4011",
|
|
981
|
+
"RECOVERY_NOT_SUPPORTED": "E4012",
|
|
982
|
+
"SESSION_EXPIRED": "E4013",
|
|
983
|
+
"CHECKPOINT_INVALID": "E4014",
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def detect_media_type_from_magic(data: bytes) -> Optional[str]:
|
|
988
|
+
"""Detect media type from magic bytes."""
|
|
989
|
+
for mime_type, magic_list in MEDIA_MAGIC_BYTES.items():
|
|
990
|
+
for magic in magic_list:
|
|
991
|
+
if data.startswith(magic):
|
|
992
|
+
# Special handling for RIFF-based formats
|
|
993
|
+
if magic == b"RIFF" and len(data) >= 12:
|
|
994
|
+
if data[8:12] == b"WEBP":
|
|
995
|
+
return "image/webp"
|
|
996
|
+
elif data[8:12] == b"WAVE":
|
|
997
|
+
return "audio/wav"
|
|
998
|
+
continue
|
|
999
|
+
# Special handling for MP4 (check for ftyp)
|
|
1000
|
+
if mime_type == "video/mp4" and len(data) >= 8:
|
|
1001
|
+
if b"ftyp" in data[4:8]:
|
|
1002
|
+
return "video/mp4"
|
|
1003
|
+
continue
|
|
1004
|
+
return mime_type
|
|
1005
|
+
return None
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def validate_media_magic_bytes(data: bytes, declared_type: str) -> tuple[bool, str]:
|
|
935
1009
|
"""
|
|
936
|
-
Validate
|
|
1010
|
+
Validate that media content matches declared MIME type.
|
|
937
1011
|
|
|
938
1012
|
Returns:
|
|
939
1013
|
Tuple of (is_valid, error_message)
|
|
940
1014
|
"""
|
|
1015
|
+
detected_type = detect_media_type_from_magic(data)
|
|
1016
|
+
|
|
1017
|
+
if detected_type is None:
|
|
1018
|
+
return True, "" # Can't detect, assume valid
|
|
1019
|
+
|
|
1020
|
+
# Normalize types for comparison
|
|
1021
|
+
declared_category = declared_type.split("/")[0]
|
|
1022
|
+
detected_category = detected_type.split("/")[0]
|
|
1023
|
+
|
|
1024
|
+
if declared_category != detected_category:
|
|
1025
|
+
return False, f"Media content mismatch: declared {declared_type}, detected {detected_type}"
|
|
1026
|
+
|
|
1027
|
+
return True, ""
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def validate_image_dimensions(data: bytes) -> Optional[tuple]:
|
|
1031
|
+
"""
|
|
1032
|
+
Extract image dimensions from raw bytes.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
Tuple of (width, height) or None if cannot determine.
|
|
1036
|
+
"""
|
|
1037
|
+
try:
|
|
1038
|
+
# PNG dimensions at bytes 16-24
|
|
1039
|
+
if data.startswith(b"\x89PNG"):
|
|
1040
|
+
width = int.from_bytes(data[16:20], "big")
|
|
1041
|
+
height = int.from_bytes(data[20:24], "big")
|
|
1042
|
+
return (width, height)
|
|
1043
|
+
|
|
1044
|
+
# JPEG - need to parse markers
|
|
1045
|
+
if data.startswith(b"\xff\xd8"):
|
|
1046
|
+
i = 2
|
|
1047
|
+
while i < len(data) - 8:
|
|
1048
|
+
if data[i] != 0xff:
|
|
1049
|
+
break
|
|
1050
|
+
marker = data[i + 1]
|
|
1051
|
+
if marker in (0xc0, 0xc1, 0xc2): # SOF markers
|
|
1052
|
+
height = int.from_bytes(data[i + 5:i + 7], "big")
|
|
1053
|
+
width = int.from_bytes(data[i + 7:i + 9], "big")
|
|
1054
|
+
return (width, height)
|
|
1055
|
+
length = int.from_bytes(data[i + 2:i + 4], "big")
|
|
1056
|
+
i += 2 + length
|
|
1057
|
+
|
|
1058
|
+
# GIF dimensions at bytes 6-10
|
|
1059
|
+
if data.startswith(b"GIF"):
|
|
1060
|
+
width = int.from_bytes(data[6:8], "little")
|
|
1061
|
+
height = int.from_bytes(data[8:10], "little")
|
|
1062
|
+
return (width, height)
|
|
1063
|
+
|
|
1064
|
+
except Exception:
|
|
1065
|
+
pass
|
|
1066
|
+
|
|
1067
|
+
return None
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def validate_media_input(media: dict, constraints: dict = None) -> tuple:
|
|
1071
|
+
"""
|
|
1072
|
+
Validate a media input object with enhanced v2.5 validation.
|
|
1073
|
+
|
|
1074
|
+
Returns:
|
|
1075
|
+
Tuple of (is_valid, error_message, error_code)
|
|
1076
|
+
"""
|
|
941
1077
|
constraints = constraints or {}
|
|
942
1078
|
|
|
943
1079
|
media_type = media.get("type")
|
|
944
|
-
if media_type not in ("url", "base64", "file"):
|
|
945
|
-
return False, "Invalid media type. Must be url, base64, or
|
|
1080
|
+
if media_type not in ("url", "base64", "file", "upload_ref"):
|
|
1081
|
+
return False, "Invalid media type. Must be url, base64, file, or upload_ref", None
|
|
946
1082
|
|
|
947
1083
|
if media_type == "url":
|
|
948
1084
|
url = media.get("url")
|
|
949
1085
|
if not url:
|
|
950
|
-
return False, "URL media missing 'url' field"
|
|
1086
|
+
return False, "URL media missing 'url' field", None
|
|
951
1087
|
if not url.startswith(("http://", "https://")):
|
|
952
|
-
return False, "URL must start with http:// or https://"
|
|
1088
|
+
return False, "URL must start with http:// or https://", None
|
|
953
1089
|
|
|
954
1090
|
elif media_type == "base64":
|
|
955
1091
|
mime_type = media.get("media_type")
|
|
956
1092
|
if not mime_type:
|
|
957
|
-
return False, "Base64 media missing 'media_type' field"
|
|
1093
|
+
return False, "Base64 media missing 'media_type' field", None
|
|
958
1094
|
data = media.get("data")
|
|
959
1095
|
if not data:
|
|
960
|
-
return False, "Base64 media missing 'data' field"
|
|
961
|
-
|
|
1096
|
+
return False, "Base64 media missing 'data' field", None
|
|
1097
|
+
|
|
1098
|
+
# Validate base64 and decode
|
|
962
1099
|
try:
|
|
963
|
-
base64.b64decode(data)
|
|
1100
|
+
decoded = base64.b64decode(data)
|
|
964
1101
|
except Exception:
|
|
965
|
-
return False, "Invalid base64 encoding"
|
|
1102
|
+
return False, "Invalid base64 encoding", ERROR_CODES_V25["MEDIA_DECODE_FAILED"]
|
|
966
1103
|
|
|
967
1104
|
# Check size
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if
|
|
971
|
-
return False, f"Media exceeds size limit ({
|
|
1105
|
+
category = mime_type.split("/")[0]
|
|
1106
|
+
max_size = constraints.get("max_size_bytes", MEDIA_SIZE_LIMITS.get(category, 20 * 1024 * 1024))
|
|
1107
|
+
if len(decoded) > max_size:
|
|
1108
|
+
return False, f"Media exceeds size limit ({len(decoded)} > {max_size} bytes)", ERROR_CODES_V25["MEDIA_TOO_LARGE"]
|
|
1109
|
+
|
|
1110
|
+
# Validate magic bytes
|
|
1111
|
+
is_valid, error = validate_media_magic_bytes(decoded, mime_type)
|
|
1112
|
+
if not is_valid:
|
|
1113
|
+
return False, error, ERROR_CODES_V25["MEDIA_TYPE_MISMATCH"]
|
|
1114
|
+
|
|
1115
|
+
# Validate image dimensions if applicable
|
|
1116
|
+
if category == "image":
|
|
1117
|
+
dimensions = validate_image_dimensions(decoded)
|
|
1118
|
+
if dimensions:
|
|
1119
|
+
width, height = dimensions
|
|
1120
|
+
limits = MEDIA_DIMENSION_LIMITS
|
|
1121
|
+
|
|
1122
|
+
if width > limits["max_width"] or height > limits["max_height"]:
|
|
1123
|
+
return False, f"Image dimensions ({width}x{height}) exceed maximum ({limits['max_width']}x{limits['max_height']})", ERROR_CODES_V25["MEDIA_DIMENSION_EXCEEDED"]
|
|
1124
|
+
|
|
1125
|
+
if width < limits["min_width"] or height < limits["min_height"]:
|
|
1126
|
+
return False, f"Image dimensions ({width}x{height}) below minimum ({limits['min_width']}x{limits['min_height']})", ERROR_CODES_V25["MEDIA_DIMENSION_TOO_SMALL"]
|
|
1127
|
+
|
|
1128
|
+
if width * height > limits["max_pixels"]:
|
|
1129
|
+
return False, f"Image pixel count ({width * height}) exceeds maximum ({limits['max_pixels']})", ERROR_CODES_V25["MEDIA_PIXEL_LIMIT"]
|
|
1130
|
+
|
|
1131
|
+
# Validate checksum if provided
|
|
1132
|
+
checksum = media.get("checksum")
|
|
1133
|
+
if checksum:
|
|
1134
|
+
import hashlib
|
|
1135
|
+
algorithm = checksum.get("algorithm", "sha256")
|
|
1136
|
+
expected = checksum.get("value", "")
|
|
1137
|
+
|
|
1138
|
+
if algorithm == "sha256":
|
|
1139
|
+
actual = hashlib.sha256(decoded).hexdigest()
|
|
1140
|
+
elif algorithm == "md5":
|
|
1141
|
+
actual = hashlib.md5(decoded).hexdigest()
|
|
1142
|
+
elif algorithm == "crc32":
|
|
1143
|
+
import zlib
|
|
1144
|
+
actual = format(zlib.crc32(decoded) & 0xffffffff, '08x')
|
|
1145
|
+
else:
|
|
1146
|
+
return False, f"Unsupported checksum algorithm: {algorithm}", None
|
|
1147
|
+
|
|
1148
|
+
if actual.lower() != expected.lower():
|
|
1149
|
+
return False, f"Checksum mismatch: expected {expected}, got {actual}", ERROR_CODES_V25["CHECKSUM_MISMATCH"]
|
|
972
1150
|
|
|
973
1151
|
elif media_type == "file":
|
|
974
1152
|
path = media.get("path")
|
|
975
1153
|
if not path:
|
|
976
|
-
return False, "File media missing 'path' field"
|
|
1154
|
+
return False, "File media missing 'path' field", None
|
|
977
1155
|
if not Path(path).exists():
|
|
978
|
-
return False, f"File not found: {path}"
|
|
1156
|
+
return False, f"File not found: {path}", None
|
|
1157
|
+
|
|
1158
|
+
# Check file size
|
|
1159
|
+
file_size = Path(path).stat().st_size
|
|
1160
|
+
mime, _ = mimetypes.guess_type(str(path))
|
|
1161
|
+
if mime:
|
|
1162
|
+
category = mime.split("/")[0]
|
|
1163
|
+
max_size = constraints.get("max_size_bytes", MEDIA_SIZE_LIMITS.get(category, 20 * 1024 * 1024))
|
|
1164
|
+
if file_size > max_size:
|
|
1165
|
+
return False, f"File exceeds size limit ({file_size} > {max_size} bytes)", ERROR_CODES_V25["MEDIA_TOO_LARGE"]
|
|
979
1166
|
|
|
980
|
-
|
|
1167
|
+
elif media_type == "upload_ref":
|
|
1168
|
+
upload_id = media.get("upload_id")
|
|
1169
|
+
if not upload_id:
|
|
1170
|
+
return False, "Upload reference missing 'upload_id' field", None
|
|
1171
|
+
# Note: Actual upload validation would require backend lookup
|
|
1172
|
+
|
|
1173
|
+
return True, "", None
|
|
981
1174
|
|
|
982
1175
|
|
|
983
1176
|
def load_media_as_base64(media: dict) -> tuple[str, str]:
|
|
@@ -1113,9 +1306,9 @@ def validate_multimodal_input(input_data: dict, module: dict) -> tuple[bool, lis
|
|
|
1113
1306
|
errors.append(f"Too many images ({len(images)} > {max_images})")
|
|
1114
1307
|
|
|
1115
1308
|
for i, img in enumerate(images):
|
|
1116
|
-
valid, err = validate_media_input(img, constraints)
|
|
1309
|
+
valid, err, err_code = validate_media_input(img, constraints)
|
|
1117
1310
|
if not valid:
|
|
1118
|
-
errors.append(f"Image {i}: {err}")
|
|
1311
|
+
errors.append(f"Image {i}: {err}" + (f" [{err_code}]" if err_code else ""))
|
|
1119
1312
|
|
|
1120
1313
|
# Check audio
|
|
1121
1314
|
audio = input_data.get("audio", [])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/requires.txt
RENAMED
|
File without changes
|
{cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|