cognitive-modules 0.6.1__py3-none-any.whl → 2.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.
cognitive/runner.py CHANGED
@@ -10,13 +10,8 @@ v2.2 Features:
10
10
  """
11
11
 
12
12
  import json
13
- import base64
14
- import mimetypes
15
13
  from pathlib import Path
16
- from typing import Optional, TypedDict, Union, Literal, Callable, AsyncIterator
17
- from dataclasses import dataclass, field
18
- from urllib.request import urlopen
19
- from urllib.error import URLError
14
+ from typing import Optional, TypedDict, Union, Literal
20
15
 
21
16
  import jsonschema
22
17
  import yaml
@@ -657,692 +652,3 @@ def should_escalate(result: EnvelopeResponseV22, confidence_threshold: float = 0
657
652
  return True
658
653
 
659
654
  return False
660
-
661
-
662
- # =============================================================================
663
- # v2.5 Streaming Support
664
- # =============================================================================
665
-
666
- import uuid
667
- from typing import AsyncIterator, Iterator, Any, Callable
668
- from dataclasses import dataclass, field
669
-
670
-
671
- @dataclass
672
- class StreamingSession:
673
- """Represents an active streaming session."""
674
- session_id: str
675
- module_name: str
676
- started_at: float = field(default_factory=lambda: __import__('time').time())
677
- chunks_sent: int = 0
678
- accumulated_data: dict = field(default_factory=dict)
679
- accumulated_text: dict = field(default_factory=dict) # field -> accumulated string
680
-
681
-
682
- def create_session_id() -> str:
683
- """Generate a unique session ID for streaming."""
684
- return f"sess_{uuid.uuid4().hex[:12]}"
685
-
686
-
687
- def create_meta_chunk(session_id: str, initial_risk: str = "low") -> dict:
688
- """Create the initial meta chunk for streaming."""
689
- return {
690
- "ok": True,
691
- "streaming": True,
692
- "session_id": session_id,
693
- "meta": {
694
- "confidence": None,
695
- "risk": initial_risk,
696
- "explain": "Processing..."
697
- }
698
- }
699
-
700
-
701
- def create_delta_chunk(seq: int, field: str, delta: str) -> dict:
702
- """Create a delta chunk for incremental content."""
703
- return {
704
- "chunk": {
705
- "seq": seq,
706
- "type": "delta",
707
- "field": field,
708
- "delta": delta
709
- }
710
- }
711
-
712
-
713
- def create_snapshot_chunk(seq: int, field: str, data: Any) -> dict:
714
- """Create a snapshot chunk for full field replacement."""
715
- return {
716
- "chunk": {
717
- "seq": seq,
718
- "type": "snapshot",
719
- "field": field,
720
- "data": data
721
- }
722
- }
723
-
724
-
725
- def create_progress_chunk(percent: int, stage: str = "", message: str = "") -> dict:
726
- """Create a progress update chunk."""
727
- return {
728
- "progress": {
729
- "percent": percent,
730
- "stage": stage,
731
- "message": message
732
- }
733
- }
734
-
735
-
736
- def create_final_chunk(meta: dict, data: dict, usage: dict = None) -> dict:
737
- """Create the final chunk with complete data."""
738
- chunk = {
739
- "final": True,
740
- "meta": meta,
741
- "data": data
742
- }
743
- if usage:
744
- chunk["usage"] = usage
745
- return chunk
746
-
747
-
748
- def create_error_chunk(session_id: str, error_code: str, message: str,
749
- recoverable: bool = False, partial_data: dict = None) -> dict:
750
- """Create an error chunk for stream failures."""
751
- chunk = {
752
- "ok": False,
753
- "streaming": True,
754
- "session_id": session_id,
755
- "error": {
756
- "code": error_code,
757
- "message": message,
758
- "recoverable": recoverable
759
- }
760
- }
761
- if partial_data:
762
- chunk["partial_data"] = partial_data
763
- return chunk
764
-
765
-
766
- def assemble_streamed_data(session: StreamingSession) -> dict:
767
- """Assemble accumulated streaming data into final format."""
768
- data = session.accumulated_data.copy()
769
-
770
- # Merge accumulated text fields
771
- for field_path, text in session.accumulated_text.items():
772
- parts = field_path.split(".")
773
- target = data
774
- for part in parts[:-1]:
775
- if part not in target:
776
- target[part] = {}
777
- target = target[part]
778
- target[parts[-1]] = text
779
-
780
- return data
781
-
782
-
783
- class StreamingRunner:
784
- """Runner with streaming support for v2.5 modules."""
785
-
786
- def __init__(self, provider_callback: Callable = None):
787
- """
788
- Initialize streaming runner.
789
-
790
- Args:
791
- provider_callback: Function to call LLM with streaming support.
792
- Signature: async (prompt, images=None) -> AsyncIterator[str]
793
- """
794
- self.provider_callback = provider_callback or self._default_provider
795
- self.active_sessions: dict[str, StreamingSession] = {}
796
-
797
- async def _default_provider(self, prompt: str, images: list = None) -> AsyncIterator[str]:
798
- """Default provider - yields entire response at once (for testing)."""
799
- # In real implementation, this would stream from LLM
800
- yield '{"ok": true, "meta": {"confidence": 0.9, "risk": "low", "explain": "Test"}, "data": {"rationale": "Test response"}}'
801
-
802
- async def execute_stream(
803
- self,
804
- module_name: str,
805
- input_data: dict,
806
- on_chunk: Callable[[dict], None] = None
807
- ) -> AsyncIterator[dict]:
808
- """
809
- Execute a module with streaming output.
810
-
811
- Args:
812
- module_name: Name of the module to execute
813
- input_data: Input data including multimodal content
814
- on_chunk: Optional callback for each chunk
815
-
816
- Yields:
817
- Streaming chunks (meta, delta, progress, final, or error)
818
- """
819
- session_id = create_session_id()
820
- session = StreamingSession(session_id=session_id, module_name=module_name)
821
- self.active_sessions[session_id] = session
822
-
823
- try:
824
- # Load module
825
- module = load_module(module_name)
826
-
827
- # Check if module supports streaming
828
- response_config = module.get("response", {})
829
- mode = response_config.get("mode", "sync")
830
- if mode not in ("streaming", "both"):
831
- # Fall back to sync execution
832
- result = await self._execute_sync(module, input_data)
833
- yield create_meta_chunk(session_id)
834
- yield create_final_chunk(result["meta"], result["data"])
835
- return
836
-
837
- # Extract images for multimodal
838
- images = self._extract_media(input_data)
839
-
840
- # Build prompt
841
- prompt = self._build_prompt(module, input_data)
842
-
843
- # Send initial meta chunk
844
- meta_chunk = create_meta_chunk(session_id)
845
- if on_chunk:
846
- on_chunk(meta_chunk)
847
- yield meta_chunk
848
-
849
- # Stream from LLM
850
- seq = 1
851
- accumulated_response = ""
852
-
853
- async for text_chunk in self.provider_callback(prompt, images):
854
- accumulated_response += text_chunk
855
-
856
- # Create delta chunk for rationale field
857
- delta_chunk = create_delta_chunk(seq, "data.rationale", text_chunk)
858
- session.chunks_sent += 1
859
- session.accumulated_text.setdefault("data.rationale", "")
860
- session.accumulated_text["data.rationale"] += text_chunk
861
-
862
- if on_chunk:
863
- on_chunk(delta_chunk)
864
- yield delta_chunk
865
- seq += 1
866
-
867
- # Parse final response
868
- try:
869
- final_data = parse_llm_response(accumulated_response)
870
- final_data = repair_envelope(final_data)
871
- except Exception as e:
872
- error_chunk = create_error_chunk(
873
- session_id, "E2001", str(e),
874
- recoverable=False,
875
- partial_data={"rationale": session.accumulated_text.get("data.rationale", "")}
876
- )
877
- yield error_chunk
878
- return
879
-
880
- # Send final chunk
881
- final_chunk = create_final_chunk(
882
- final_data.get("meta", {}),
883
- final_data.get("data", {}),
884
- {"input_tokens": 0, "output_tokens": seq} # Placeholder
885
- )
886
- if on_chunk:
887
- on_chunk(final_chunk)
888
- yield final_chunk
889
-
890
- except Exception as e:
891
- error_chunk = create_error_chunk(
892
- session_id, "E2010", f"Stream error: {str(e)}",
893
- recoverable=False
894
- )
895
- yield error_chunk
896
- finally:
897
- del self.active_sessions[session_id]
898
-
899
- async def _execute_sync(self, module: dict, input_data: dict) -> dict:
900
- """Execute module synchronously (fallback)."""
901
- # Use existing sync execution
902
- return run_module(module["name"], input_data)
903
-
904
- def _build_prompt(self, module: dict, input_data: dict) -> str:
905
- """Build prompt from module and input."""
906
- prompt_template = module.get("prompt", "")
907
- return substitute_arguments(prompt_template, input_data)
908
-
909
- def _extract_media(self, input_data: dict) -> list:
910
- """Extract media inputs from input data."""
911
- images = input_data.get("images", [])
912
- audio = input_data.get("audio", [])
913
- video = input_data.get("video", [])
914
- return images + audio + video
915
-
916
-
917
- # =============================================================================
918
- # v2.5 Multimodal Support
919
- # =============================================================================
920
-
921
- SUPPORTED_IMAGE_TYPES = {
922
- "image/jpeg", "image/png", "image/webp", "image/gif"
923
- }
924
-
925
- SUPPORTED_AUDIO_TYPES = {
926
- "audio/mpeg", "audio/wav", "audio/ogg", "audio/webm"
927
- }
928
-
929
- SUPPORTED_VIDEO_TYPES = {
930
- "video/mp4", "video/webm", "video/quicktime"
931
- }
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
- }
954
-
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]:
1009
- """
1010
- Validate that media content matches declared MIME type.
1011
-
1012
- Returns:
1013
- Tuple of (is_valid, error_message)
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
- """
1077
- constraints = constraints or {}
1078
-
1079
- media_type = media.get("type")
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
1082
-
1083
- if media_type == "url":
1084
- url = media.get("url")
1085
- if not url:
1086
- return False, "URL media missing 'url' field", None
1087
- if not url.startswith(("http://", "https://")):
1088
- return False, "URL must start with http:// or https://", None
1089
-
1090
- elif media_type == "base64":
1091
- mime_type = media.get("media_type")
1092
- if not mime_type:
1093
- return False, "Base64 media missing 'media_type' field", None
1094
- data = media.get("data")
1095
- if not data:
1096
- return False, "Base64 media missing 'data' field", None
1097
-
1098
- # Validate base64 and decode
1099
- try:
1100
- decoded = base64.b64decode(data)
1101
- except Exception:
1102
- return False, "Invalid base64 encoding", ERROR_CODES_V25["MEDIA_DECODE_FAILED"]
1103
-
1104
- # Check size
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"]
1150
-
1151
- elif media_type == "file":
1152
- path = media.get("path")
1153
- if not path:
1154
- return False, "File media missing 'path' field", None
1155
- if not Path(path).exists():
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"]
1166
-
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
1174
-
1175
-
1176
- def load_media_as_base64(media: dict) -> tuple[str, str]:
1177
- """
1178
- Load media from any source and return as base64.
1179
-
1180
- Returns:
1181
- Tuple of (base64_data, media_type)
1182
- """
1183
- media_type = media.get("type")
1184
-
1185
- if media_type == "base64":
1186
- return media["data"], media["media_type"]
1187
-
1188
- elif media_type == "url":
1189
- url = media["url"]
1190
- try:
1191
- with urlopen(url, timeout=30) as response:
1192
- data = response.read()
1193
- content_type = response.headers.get("Content-Type", "application/octet-stream")
1194
- # Extract just the mime type (remove charset etc)
1195
- content_type = content_type.split(";")[0].strip()
1196
- return base64.b64encode(data).decode("utf-8"), content_type
1197
- except URLError as e:
1198
- raise ValueError(f"Failed to fetch media from URL: {e}")
1199
-
1200
- elif media_type == "file":
1201
- path = Path(media["path"])
1202
- if not path.exists():
1203
- raise ValueError(f"File not found: {path}")
1204
-
1205
- mime_type, _ = mimetypes.guess_type(str(path))
1206
- mime_type = mime_type or "application/octet-stream"
1207
-
1208
- with open(path, "rb") as f:
1209
- data = f.read()
1210
-
1211
- return base64.b64encode(data).decode("utf-8"), mime_type
1212
-
1213
- raise ValueError(f"Unknown media type: {media_type}")
1214
-
1215
-
1216
- def prepare_media_for_llm(media_list: list, provider: str = "openai") -> list:
1217
- """
1218
- Prepare media inputs for specific LLM provider format.
1219
-
1220
- Different providers have different multimodal input formats:
1221
- - OpenAI: {"type": "image_url", "image_url": {"url": "data:..."}}
1222
- - Anthropic: {"type": "image", "source": {"type": "base64", ...}}
1223
- - Google: {"inlineData": {"mimeType": "...", "data": "..."}}
1224
- """
1225
- prepared = []
1226
-
1227
- for media in media_list:
1228
- data, mime_type = load_media_as_base64(media)
1229
-
1230
- if provider == "openai":
1231
- prepared.append({
1232
- "type": "image_url",
1233
- "image_url": {
1234
- "url": f"data:{mime_type};base64,{data}"
1235
- }
1236
- })
1237
- elif provider == "anthropic":
1238
- prepared.append({
1239
- "type": "image",
1240
- "source": {
1241
- "type": "base64",
1242
- "media_type": mime_type,
1243
- "data": data
1244
- }
1245
- })
1246
- elif provider == "google":
1247
- prepared.append({
1248
- "inlineData": {
1249
- "mimeType": mime_type,
1250
- "data": data
1251
- }
1252
- })
1253
- else:
1254
- # Generic format
1255
- prepared.append({
1256
- "type": "base64",
1257
- "media_type": mime_type,
1258
- "data": data
1259
- })
1260
-
1261
- return prepared
1262
-
1263
-
1264
- def get_modalities_config(module: dict) -> dict:
1265
- """Get modalities configuration from module."""
1266
- return module.get("modalities", {
1267
- "input": ["text"],
1268
- "output": ["text"]
1269
- })
1270
-
1271
-
1272
- def supports_multimodal_input(module: dict) -> bool:
1273
- """Check if module supports multimodal input."""
1274
- modalities = get_modalities_config(module)
1275
- input_modalities = modalities.get("input", ["text"])
1276
- return any(m in input_modalities for m in ["image", "audio", "video"])
1277
-
1278
-
1279
- def supports_multimodal_output(module: dict) -> bool:
1280
- """Check if module supports multimodal output."""
1281
- modalities = get_modalities_config(module)
1282
- output_modalities = modalities.get("output", ["text"])
1283
- return any(m in output_modalities for m in ["image", "audio", "video"])
1284
-
1285
-
1286
- def validate_multimodal_input(input_data: dict, module: dict) -> tuple[bool, list[str]]:
1287
- """
1288
- Validate multimodal input against module configuration.
1289
-
1290
- Returns:
1291
- Tuple of (is_valid, list of errors)
1292
- """
1293
- errors = []
1294
- modalities = get_modalities_config(module)
1295
- input_modalities = set(modalities.get("input", ["text"]))
1296
- constraints = modalities.get("constraints", {})
1297
-
1298
- # Check images
1299
- images = input_data.get("images", [])
1300
- if images:
1301
- if "image" not in input_modalities:
1302
- errors.append("Module does not support image input")
1303
- else:
1304
- max_images = constraints.get("max_images", 10)
1305
- if len(images) > max_images:
1306
- errors.append(f"Too many images ({len(images)} > {max_images})")
1307
-
1308
- for i, img in enumerate(images):
1309
- valid, err, err_code = validate_media_input(img, constraints)
1310
- if not valid:
1311
- errors.append(f"Image {i}: {err}" + (f" [{err_code}]" if err_code else ""))
1312
-
1313
- # Check audio
1314
- audio = input_data.get("audio", [])
1315
- if audio:
1316
- if "audio" not in input_modalities:
1317
- errors.append("Module does not support audio input")
1318
-
1319
- # Check video
1320
- video = input_data.get("video", [])
1321
- if video:
1322
- if "video" not in input_modalities:
1323
- errors.append("Module does not support video input")
1324
-
1325
- return len(errors) == 0, errors
1326
-
1327
-
1328
- # =============================================================================
1329
- # v2.5 Runtime Capabilities
1330
- # =============================================================================
1331
-
1332
- def get_runtime_capabilities() -> dict:
1333
- """Get runtime capabilities for v2.5."""
1334
- return {
1335
- "runtime": "cognitive-runtime-python",
1336
- "version": "2.5.0",
1337
- "spec_version": "2.5",
1338
- "capabilities": {
1339
- "streaming": True,
1340
- "multimodal": {
1341
- "input": ["image"], # Basic image support
1342
- "output": [] # No generation yet
1343
- },
1344
- "max_media_size_mb": 20,
1345
- "supported_transports": ["ndjson"], # SSE requires async server
1346
- "conformance_level": 4
1347
- }
1348
- }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognitive-modules
3
- Version: 0.6.1
3
+ Version: 2.2.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
@@ -69,32 +69,6 @@ English | [中文](README_zh.md)
69
69
 
70
70
  Cognitive Modules is an AI task definition specification designed for generation tasks that require **strong constraints, verifiability, and auditability**.
71
71
 
72
- ## What's New in v2.5
73
-
74
- | Feature | Description |
75
- |---------|-------------|
76
- | **Streaming Response** | Real-time chunk-based output for better UX |
77
- | **Multimodal Support** | Native image, audio, and video input/output |
78
- | **Backward Compatible** | v2.2 modules run without modification |
79
- | **Level 4 Conformance** | Extended conformance level for v2.5 features |
80
-
81
- ```yaml
82
- # module.yaml - v2.5 example
83
- name: image-analyzer
84
- version: 2.5.0
85
- tier: decision
86
-
87
- # v2.5: Enable streaming
88
- response:
89
- mode: streaming
90
- chunk_type: delta
91
-
92
- # v2.5: Enable multimodal
93
- modalities:
94
- input: [text, image]
95
- output: [text]
96
- ```
97
-
98
72
  ## What's New in v2.2
99
73
 
100
74
  | Feature | Description |
@@ -114,7 +88,24 @@ modalities:
114
88
  - **Subagent Orchestration** - `@call:module` supports inter-module calls
115
89
  - **Parameter Passing** - `$ARGUMENTS` runtime substitution
116
90
  - **Multi-LLM Support** - OpenAI / Anthropic / MiniMax / Ollama
117
- - **Public Registry** - `cogn install registry:module-name`
91
+ - **Public Registry** - `cog install registry:module-name`
92
+
93
+ ## Version Selection
94
+
95
+ | Version | Spec | npm | PyPI | Status |
96
+ |---------|------|-----|------|--------|
97
+ | **v2.2** | v2.2 | `2.2.0` | `2.2.0` | ✅ Stable (recommended) |
98
+ | **v2.5** | v2.5 | `2.5.0-beta.x` | `2.5.0bx` | 🧪 Beta (streaming + multimodal) |
99
+
100
+ ```bash
101
+ # Install stable v2.2
102
+ npm install cognitive-modules-cli@2.2.0
103
+ pip install cognitive-modules==2.2.0
104
+
105
+ # Install beta v2.5 (streaming + multimodal)
106
+ npm install cognitive-modules-cli@beta
107
+ pip install cognitive-modules==2.5.0b1
108
+ ```
118
109
 
119
110
  ## Installation
120
111
 
@@ -122,30 +113,27 @@ modalities:
122
113
 
123
114
  ```bash
124
115
  # Zero-install quick start (recommended)
125
- npx cogn run code-reviewer --args "your code"
116
+ npx cognitive-modules-cli@2.2.0 run code-reviewer --args "your code"
126
117
 
127
118
  # Global installation
128
- npm install -g cogn
129
-
130
- # Or install with full package name
131
- npm install -g cognitive-modules-cli
119
+ npm install -g cognitive-modules-cli@2.2.0
132
120
  ```
133
121
 
134
122
  ### Python (pip)
135
123
 
136
124
  ```bash
137
- pip install cognitive-modules
125
+ pip install cognitive-modules==2.2.0
138
126
 
139
127
  # With LLM support
140
- pip install cognitive-modules[openai] # OpenAI
141
- pip install cognitive-modules[anthropic] # Claude
142
- pip install cognitive-modules[all] # All providers
128
+ pip install "cognitive-modules[openai]==2.2.0" # OpenAI
129
+ pip install "cognitive-modules[anthropic]==2.2.0" # Claude
130
+ pip install "cognitive-modules[all]==2.2.0" # All providers
143
131
  ```
144
132
 
145
133
  | Platform | Package | Command | Features |
146
134
  |----------|---------|---------|----------|
147
- | **npm** | `cogn` | `cog` | ✅ Recommended, zero-install, full features |
148
- | pip | `cognitive-modules` | `cogn` | ✅ Full features |
135
+ | **npm** | `cognitive-modules-cli` | `cog` | ✅ Recommended, zero-install, full features |
136
+ | pip | `cognitive-modules` | `cog` | ✅ Full features |
149
137
 
150
138
  ## Quick Start
151
139
 
@@ -155,7 +143,7 @@ export LLM_PROVIDER=openai
155
143
  export OPENAI_API_KEY=sk-xxx
156
144
 
157
145
  # Run code review (npm)
158
- npx cogn run code-reviewer --args "def login(u,p): return db.query(f'SELECT * FROM users WHERE name={u}')" --pretty
146
+ npx cognitive-modules-cli run code-reviewer --args "def login(u,p): return db.query(f'SELECT * FROM users WHERE name={u}')" --pretty
159
147
 
160
148
  # Or use globally installed cog command
161
149
  cog run code-reviewer --args "..." --pretty
@@ -221,70 +209,70 @@ All modules now return the unified v2.2 envelope format:
221
209
  | **Risk Aggregation** | `meta.risk = max(changes[*].risk)` |
222
210
  | **Parameter Passing** | `$ARGUMENTS` runtime substitution |
223
211
  | **Subagents** | `@call:module` for inter-module calls |
224
- | **Validation Tools** | `cogn validate` / `cogn validate --v22` |
212
+ | **Validation Tools** | `cog validate` / `cog validate --v22` |
225
213
 
226
214
  ## Integration Methods
227
215
 
228
216
  | Method | Command | Use Case |
229
217
  |--------|---------|----------|
230
- | CLI | `cogn run` | Command line |
231
- | HTTP API | `cogn serve` | n8n, Coze, Dify |
232
- | MCP Server | `cogn mcp` | Claude, Cursor |
218
+ | CLI | `cog run` | Command line |
219
+ | HTTP API | `cog serve` | n8n, Coze, Dify |
220
+ | MCP Server | `cog mcp` | Claude, Cursor |
233
221
 
234
222
  ## CLI Commands
235
223
 
236
224
  ```bash
237
225
  # Module management
238
- cogn list # List installed modules
239
- cogn info <module> # View module details
240
- cogn validate <module> # Validate module structure
241
- cogn validate <module> --v22 # Validate v2.2 format
226
+ cog list # List installed modules
227
+ cog info <module> # View module details
228
+ cog validate <module> # Validate module structure
229
+ cog validate <module> --v22 # Validate v2.2 format
242
230
 
243
231
  # Run modules
244
- cogn run <module> input.json -o output.json --pretty
245
- cogn run <module> --args "requirements" --pretty
246
- cogn run <module> --args "requirements" --subagent # Enable subagent
232
+ cog run <module> input.json -o output.json --pretty
233
+ cog run <module> --args "requirements" --pretty
234
+ cog run <module> --args "requirements" --subagent # Enable subagent
247
235
 
248
236
  # Create modules
249
- cogn init <name> -d "description"
250
- cogn init <name> --format v22 # Create v2.2 format module
237
+ cog init <name> -d "description"
238
+ cog init <name> --format v22 # Create v2.2 format module
251
239
 
252
240
  # Migrate modules
253
- cogn migrate <module> # Migrate v1/v2.1 module to v2.2
241
+ cog migrate <module> # Migrate v1/v2.1 module to v2.2
254
242
 
255
243
  # Install from GitHub (recommended)
256
- cogn add ziel-io/cognitive-modules -m code-simplifier
257
- cogn add org/repo -m module-name --tag v1.0.0 # Install specific version
258
- cogn remove <module> # Remove module
244
+ cog add ziel-io/cognitive-modules -m code-simplifier
245
+ cog add org/repo -m module-name --tag v1.0.0 # Install specific version
246
+ cog remove <module> # Remove module
259
247
 
260
248
  # Version management
261
- cogn update <module> # Update to latest version
262
- cogn update <module> --tag v2.0.0 # Update to specific version
263
- cogn versions <url> # View available versions
249
+ cog update <module> # Update to latest version
250
+ cog update <module> --tag v2.0.0 # Update to specific version
251
+ cog versions <url> # View available versions
264
252
 
265
253
  # Other installation methods
266
- cogn install github:user/repo/path
267
- cogn install registry:module-name
268
- cogn uninstall <module>
254
+ cog install github:user/repo/path
255
+ cog install registry:module-name
256
+ cog uninstall <module>
269
257
 
270
258
  # Registry
271
- cogn registry # View public modules
272
- cogn search <query> # Search modules
259
+ cog registry # View public modules
260
+ cog search <query> # Search modules
273
261
 
274
262
  # Environment check
275
- cogn doctor
263
+ cog doctor
276
264
  ```
277
265
 
278
266
  ## Built-in Modules
279
267
 
280
268
  | Module | Tier | Function | Example |
281
269
  |--------|------|----------|---------|
282
- | `code-reviewer` | decision | Code review | `cogn run code-reviewer --args "your code"` |
283
- | `code-simplifier` | decision | Code simplification | `cogn run code-simplifier --args "complex code"` |
284
- | `task-prioritizer` | decision | Task priority sorting | `cogn run task-prioritizer --args "task1,task2"` |
285
- | `api-designer` | decision | REST API design | `cogn run api-designer --args "order system"` |
286
- | `ui-spec-generator` | exploration | UI spec generation | `cogn run ui-spec-generator --args "e-commerce homepage"` |
287
- | `product-analyzer` | exploration | Product analysis (subagent) | `cogn run product-analyzer --args "health product" -s` |
270
+ | `code-reviewer` | decision | Code review | `cog run code-reviewer --args "your code"` |
271
+ | `code-simplifier` | decision | Code simplification | `cog run code-simplifier --args "complex code"` |
272
+ | `task-prioritizer` | decision | Task priority sorting | `cog run task-prioritizer --args "task1,task2"` |
273
+ | `api-designer` | decision | REST API design | `cog run api-designer --args "order system"` |
274
+ | `ui-spec-generator` | exploration | UI spec generation | `cog run ui-spec-generator --args "e-commerce homepage"` |
275
+ | `product-analyzer` | exploration | Product analysis (subagent) | `cog run product-analyzer --args "health product" -s` |
288
276
 
289
277
  ## Module Format
290
278
 
@@ -355,83 +343,6 @@ my-module/
355
343
  | `decision` | Judgment/evaluation/classification | medium | Enabled |
356
344
  | `exploration` | Exploration/research/inspiration | low | Enabled |
357
345
 
358
- ## v2.5 Streaming & Multimodal
359
-
360
- ### Streaming Response
361
-
362
- Enable real-time streaming for better UX:
363
-
364
- ```yaml
365
- # module.yaml
366
- response:
367
- mode: streaming # sync | streaming | both
368
- chunk_type: delta # delta | snapshot
369
- ```
370
-
371
- **JavaScript/TypeScript Usage:**
372
-
373
- ```typescript
374
- import { runModuleStream } from 'cognitive-modules-cli';
375
-
376
- // Stream execution
377
- for await (const chunk of runModuleStream(module, provider, { input })) {
378
- if ('delta' in chunk.chunk) {
379
- process.stdout.write(chunk.chunk.delta);
380
- } else if ('final' in chunk) {
381
- console.log('\nComplete:', chunk.data);
382
- }
383
- }
384
- ```
385
-
386
- **Python Usage:**
387
-
388
- ```python
389
- from cognitive import StreamingRunner
390
-
391
- runner = StreamingRunner()
392
- async for chunk in runner.execute_stream("image-analyzer", input_data):
393
- if chunk.get("chunk"):
394
- print(chunk["chunk"]["delta"], end="")
395
- elif chunk.get("final"):
396
- print("\nComplete:", chunk["data"])
397
- ```
398
-
399
- ### Multimodal Input
400
-
401
- Enable image/audio/video processing:
402
-
403
- ```yaml
404
- # module.yaml
405
- modalities:
406
- input: [text, image, audio]
407
- output: [text, image]
408
- ```
409
-
410
- **JavaScript/TypeScript Usage:**
411
-
412
- ```typescript
413
- const result = await runModule(module, provider, {
414
- input: {
415
- prompt: "Describe this image",
416
- images: [
417
- { type: "url", url: "https://example.com/image.jpg" },
418
- { type: "base64", media_type: "image/png", data: "iVBORw0K..." }
419
- ]
420
- }
421
- });
422
- ```
423
-
424
- **Python Usage:**
425
-
426
- ```python
427
- result = await runner.execute("image-analyzer", {
428
- "prompt": "Describe this image",
429
- "images": [
430
- {"type": "url", "url": "https://example.com/image.jpg"},
431
- {"type": "file", "path": "./local-image.png"}
432
- ]
433
- })
434
-
435
346
  ## Using with AI Tools
436
347
 
437
348
  ### Cursor / Codex CLI
@@ -473,7 +384,7 @@ export MINIMAX_API_KEY=sk-xxx
473
384
  export LLM_PROVIDER=ollama
474
385
 
475
386
  # Check configuration
476
- cogn doctor
387
+ cog doctor
477
388
  ```
478
389
 
479
390
  ## Migrating to v2.2
@@ -482,13 +393,13 @@ Migrate from v1 or v2.1 modules to v2.2:
482
393
 
483
394
  ```bash
484
395
  # Auto-migrate single module
485
- cogn migrate code-reviewer
396
+ cog migrate code-reviewer
486
397
 
487
398
  # Migrate all modules
488
- cogn migrate --all
399
+ cog migrate --all
489
400
 
490
401
  # Verify migration result
491
- cogn validate code-reviewer --v22
402
+ cog validate code-reviewer --v22
492
403
  ```
493
404
 
494
405
  Manual migration steps:
@@ -511,8 +422,8 @@ pip install -e ".[dev]"
511
422
  pytest tests/ -v
512
423
 
513
424
  # Create new module (v2.2 format)
514
- cogn init my-module -d "module description" --format v22
515
- cogn validate my-module --v22
425
+ cog init my-module -d "module description" --format v22
426
+ cog validate my-module --v22
516
427
  ```
517
428
 
518
429
  ## Project Structure
@@ -546,8 +457,8 @@ cognitive-modules/
546
457
 
547
458
  | Platform | Package | Command | Installation |
548
459
  |----------|---------|---------|--------------|
549
- | Python | `cognitive-modules` | `cogn` | `pip install cognitive-modules` |
550
- | Node.js | `cogn` or `cognitive-modules-cli` | `cog` | `npm install -g cogn` or `npx cogn` |
460
+ | Python | `cognitive-modules` | `cog` | `pip install cognitive-modules` |
461
+ | Node.js | `cognitive-modules-cli` | `cog` | `npm install -g cognitive-modules-cli` |
551
462
 
552
463
  Both versions share the same module format and v2.2 specification.
553
464
 
@@ -557,8 +468,6 @@ Both versions share the same module format and v2.2 specification.
557
468
 
558
469
  | Document | Description |
559
470
  |----------|-------------|
560
- | [SPEC-v2.5.md](SPEC-v2.5.md) | v2.5 full specification (Streaming, Multimodal) |
561
- | [SPEC-v2.5_zh.md](SPEC-v2.5_zh.md) | v2.5 规范中文版 |
562
471
  | [SPEC-v2.2.md](SPEC-v2.2.md) | v2.2 full specification (Control/Data separation, Tier, Overflow) |
563
472
  | [SPEC-v2.2_zh.md](SPEC-v2.2_zh.md) | v2.2 规范中文版 |
564
473
  | [SPEC.md](SPEC.md) | v0.1 specification (context philosophy) |
@@ -4,15 +4,15 @@ cognitive/loader.py,sha256=NgFNcGXO56_1T1qxdvwWrpE37NoysTpuKhcQtchnros,15052
4
4
  cognitive/mcp_server.py,sha256=NUQT7IqdYB9tQ-1YYfG5MPJuq6A70FXlcV2x3lBnuvM,6852
5
5
  cognitive/migrate.py,sha256=FzdEwQvLWxmXy09fglV0YRxK4ozNeNT9UacfxGSJPZU,20156
6
6
  cognitive/registry.py,sha256=qLGyvUxSDP4frs46UlPa5jCcKubqiikWT3Y9JrBqfFc,18549
7
- cognitive/runner.py,sha256=KGIuB1pBxDX3l68HAvFUAKKhSJ3_2_IftmxpidULlts,46462
7
+ cognitive/runner.py,sha256=87iC0eqj9U_CHJCubsYoP5OA-rXrfPibbffvDUkf00o,22114
8
8
  cognitive/server.py,sha256=cz8zGqhGrZgFY-HM0RSCjPCpB_Mfg1jsybeoo2ALvAc,9041
9
9
  cognitive/subagent.py,sha256=fb7LWwNF6YcJtC_T1dK0EvzqWMBnav-kiCIpvVohEBw,8142
10
10
  cognitive/templates.py,sha256=lKC197X9aQIA-npUvVCaplSwvhxjsH_KYVCQtrTZrL4,4712
11
11
  cognitive/validator.py,sha256=8B02JQQdNcwvH-mtbyP8bVTRQxUrO9Prt4gU0N53fdg,21843
12
12
  cognitive/providers/__init__.py,sha256=hqhVA1IEXpVtyCAteXhO5yD8a8ikQpVIPEKJVHLtRFY,7492
13
- cognitive_modules-0.6.1.dist-info/licenses/LICENSE,sha256=NXFYUy2hPJdh3NHRxMChTnMiQD9k8zFxkmR7gWefexc,1064
14
- cognitive_modules-0.6.1.dist-info/METADATA,sha256=yMxvs8uHu_V8jjtKjr50ueUecx7q562GSBQGgZ-Ixes,18880
15
- cognitive_modules-0.6.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
16
- cognitive_modules-0.6.1.dist-info/entry_points.txt,sha256=1yqKB-MjHrRoe8FcYm-TgZDqGEZxzBPsd0xC4pcfBNU,43
17
- cognitive_modules-0.6.1.dist-info/top_level.txt,sha256=kGIfDucCKylo8cRBtxER_v3DHIea-Sol9x9YSJo1u3Y,10
18
- cognitive_modules-0.6.1.dist-info/RECORD,,
13
+ cognitive_modules-2.2.1.dist-info/licenses/LICENSE,sha256=NXFYUy2hPJdh3NHRxMChTnMiQD9k8zFxkmR7gWefexc,1064
14
+ cognitive_modules-2.2.1.dist-info/METADATA,sha256=Cq2aLERDsebtIOkmGqibEEF8vzOPG2a7xRmzCDdqxe4,16975
15
+ cognitive_modules-2.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
16
+ cognitive_modules-2.2.1.dist-info/entry_points.txt,sha256=PKHlfrFmve5K2349ryipySKbOOsKxo_vIq1NNT-iBV0,42
17
+ cognitive_modules-2.2.1.dist-info/top_level.txt,sha256=kGIfDucCKylo8cRBtxER_v3DHIea-Sol9x9YSJo1u3Y,10
18
+ cognitive_modules-2.2.1.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cog = cognitive.cli:app
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- cogn = cognitive.cli:app