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.
Files changed (30) hide show
  1. {cognitive_modules-0.6.0/src/cognitive_modules.egg-info → cognitive_modules-0.6.1}/PKG-INFO +1 -1
  2. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/pyproject.toml +1 -1
  3. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/runner.py +213 -20
  4. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1/src/cognitive_modules.egg-info}/PKG-INFO +1 -1
  5. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/LICENSE +0 -0
  6. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/README.md +0 -0
  7. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/setup.cfg +0 -0
  8. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/__init__.py +0 -0
  9. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/cli.py +0 -0
  10. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/loader.py +0 -0
  11. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/mcp_server.py +0 -0
  12. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/migrate.py +0 -0
  13. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/providers/__init__.py +0 -0
  14. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/registry.py +0 -0
  15. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/server.py +0 -0
  16. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/subagent.py +0 -0
  17. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/templates.py +0 -0
  18. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive/validator.py +0 -0
  19. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/SOURCES.txt +0 -0
  20. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/dependency_links.txt +0 -0
  21. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/entry_points.txt +0 -0
  22. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/requires.txt +0 -0
  23. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/src/cognitive_modules.egg-info/top_level.txt +0 -0
  24. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_cli.py +0 -0
  25. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_loader.py +0 -0
  26. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_migrate.py +0 -0
  27. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_registry.py +0 -0
  28. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_runner.py +0 -0
  29. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_subagent.py +0 -0
  30. {cognitive_modules-0.6.0 → cognitive_modules-0.6.1}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognitive-modules
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Structured LLM task runner with schema validation, confidence scoring, and subagent orchestration
5
5
  Author: ziel-io
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cognitive-modules"
7
- version = "0.6.0"
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
- def validate_media_input(media: dict, constraints: dict = None) -> tuple[bool, str]:
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 a media input object.
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 file"
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
- # Validate base64
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
- max_size = constraints.get("max_size_bytes", 20 * 1024 * 1024) # 20MB default
969
- data_size = len(data) * 3 // 4 # Approximate decoded size
970
- if data_size > max_size:
971
- return False, f"Media exceeds size limit ({data_size} > {max_size} bytes)"
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
- return True, ""
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", [])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognitive-modules
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Structured LLM task runner with schema validation, confidence scoring, and subagent orchestration
5
5
  Author: ziel-io
6
6
  License: MIT