cognitive-modules 0.1.0__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.
@@ -0,0 +1,236 @@
1
+ """
2
+ LLM Providers - Unified interface for calling different LLM backends.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+
11
+ def call_llm(prompt: str, model: Optional[str] = None) -> str:
12
+ """
13
+ Call the configured LLM with the given prompt.
14
+
15
+ Configure via environment variables:
16
+ - LLM_PROVIDER: "openai", "anthropic", "ollama", "minimax", "stub"
17
+ - OPENAI_API_KEY / ANTHROPIC_API_KEY / MINIMAX_API_KEY
18
+ - LLM_MODEL: model override
19
+
20
+ Args:
21
+ prompt: The prompt to send
22
+ model: Optional model override
23
+
24
+ Returns:
25
+ The LLM's response as a string
26
+ """
27
+ provider = os.environ.get("LLM_PROVIDER", "stub").lower()
28
+
29
+ if provider == "openai":
30
+ return _call_openai(prompt, model)
31
+ elif provider == "anthropic":
32
+ return _call_anthropic(prompt, model)
33
+ elif provider == "ollama":
34
+ return _call_ollama(prompt, model)
35
+ elif provider == "minimax":
36
+ return _call_minimax(prompt, model)
37
+ else:
38
+ return _call_stub(prompt)
39
+
40
+
41
+ def _call_openai(prompt: str, model: Optional[str] = None) -> str:
42
+ """Call OpenAI API."""
43
+ try:
44
+ from openai import OpenAI
45
+ except ImportError:
46
+ raise ImportError("OpenAI not installed. Run: pip install cognitive[openai]")
47
+
48
+ api_key = os.environ.get("OPENAI_API_KEY")
49
+ if not api_key:
50
+ raise ValueError("OPENAI_API_KEY environment variable not set")
51
+
52
+ client = OpenAI(api_key=api_key)
53
+ model = model or os.environ.get("LLM_MODEL", "gpt-4o")
54
+
55
+ response = client.chat.completions.create(
56
+ model=model,
57
+ messages=[
58
+ {"role": "system", "content": "You output only valid JSON matching the required schema."},
59
+ {"role": "user", "content": prompt}
60
+ ],
61
+ temperature=0.2,
62
+ response_format={"type": "json_object"}
63
+ )
64
+
65
+ return response.choices[0].message.content
66
+
67
+
68
+ def _call_anthropic(prompt: str, model: Optional[str] = None) -> str:
69
+ """Call Anthropic Claude API."""
70
+ try:
71
+ import anthropic
72
+ except ImportError:
73
+ raise ImportError("Anthropic not installed. Run: pip install cognitive[anthropic]")
74
+
75
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
76
+ if not api_key:
77
+ raise ValueError("ANTHROPIC_API_KEY environment variable not set")
78
+
79
+ client = anthropic.Anthropic(api_key=api_key)
80
+ model = model or os.environ.get("LLM_MODEL", "claude-sonnet-4-20250514")
81
+
82
+ response = client.messages.create(
83
+ model=model,
84
+ max_tokens=8192,
85
+ system="You output only valid JSON matching the required schema.",
86
+ messages=[{"role": "user", "content": prompt}]
87
+ )
88
+
89
+ return response.content[0].text
90
+
91
+
92
+ def _call_minimax(prompt: str, model: Optional[str] = None) -> str:
93
+ """Call MiniMax API (OpenAI-compatible)."""
94
+ try:
95
+ from openai import OpenAI
96
+ except ImportError:
97
+ raise ImportError("OpenAI SDK not installed. Run: pip install openai")
98
+
99
+ api_key = os.environ.get("MINIMAX_API_KEY")
100
+ if not api_key:
101
+ raise ValueError("MINIMAX_API_KEY environment variable not set")
102
+
103
+ client = OpenAI(
104
+ api_key=api_key,
105
+ base_url="https://api.minimax.chat/v1"
106
+ )
107
+ model = model or os.environ.get("LLM_MODEL", "MiniMax-Text-01")
108
+
109
+ response = client.chat.completions.create(
110
+ model=model,
111
+ messages=[
112
+ {"role": "system", "content": "You output only valid JSON matching the required schema. Do not include any text before or after the JSON."},
113
+ {"role": "user", "content": prompt}
114
+ ],
115
+ temperature=0.2,
116
+ )
117
+
118
+ return response.choices[0].message.content
119
+
120
+
121
+ def _call_ollama(prompt: str, model: Optional[str] = None) -> str:
122
+ """Call local Ollama instance."""
123
+ try:
124
+ import requests
125
+ except ImportError:
126
+ raise ImportError("Requests not installed. Run: pip install cognitive[ollama]")
127
+
128
+ host = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
129
+ model = model or os.environ.get("LLM_MODEL", "llama3.1")
130
+
131
+ response = requests.post(
132
+ f"{host}/api/generate",
133
+ json={
134
+ "model": model,
135
+ "prompt": prompt,
136
+ "stream": False,
137
+ "format": "json",
138
+ "options": {"temperature": 0.2}
139
+ }
140
+ )
141
+ response.raise_for_status()
142
+
143
+ return response.json()["response"]
144
+
145
+
146
+ def _call_stub(prompt: str) -> str:
147
+ """
148
+ Stub implementation for testing without LLM.
149
+ Returns example output if available.
150
+ """
151
+ # Try to find example output from a module
152
+ # This is a heuristic - look for cognitive/modules directories
153
+ search_paths = [
154
+ Path.cwd() / "cognitive" / "modules",
155
+ Path.home() / ".cognitive" / "modules",
156
+ ]
157
+
158
+ for base in search_paths:
159
+ if not base.exists():
160
+ continue
161
+ for module_dir in base.iterdir():
162
+ if not module_dir.is_dir():
163
+ continue
164
+ prompt_file = module_dir / "prompt.txt"
165
+ output_file = module_dir / "examples" / "output.json"
166
+ if prompt_file.exists() and output_file.exists():
167
+ with open(prompt_file, 'r') as f:
168
+ module_prompt = f.read()
169
+ # Check if this module's prompt is in the request
170
+ if module_prompt[:100] in prompt:
171
+ with open(output_file, 'r') as f:
172
+ return f.read()
173
+
174
+ # Fallback minimal response
175
+ return json.dumps({
176
+ "specification": {},
177
+ "rationale": {
178
+ "decisions": [{"aspect": "stub", "decision": "stub", "reasoning": "No LLM configured"}],
179
+ "assumptions": [],
180
+ "open_questions": ["Set LLM_PROVIDER environment variable"]
181
+ },
182
+ "confidence": 0.0
183
+ })
184
+
185
+
186
+ def check_provider_status() -> dict:
187
+ """Check which providers are available and configured."""
188
+ status = {}
189
+
190
+ # OpenAI
191
+ try:
192
+ import openai
193
+ status["openai"] = {
194
+ "installed": True,
195
+ "configured": bool(os.environ.get("OPENAI_API_KEY")),
196
+ }
197
+ except ImportError:
198
+ status["openai"] = {"installed": False, "configured": False}
199
+
200
+ # Anthropic
201
+ try:
202
+ import anthropic
203
+ status["anthropic"] = {
204
+ "installed": True,
205
+ "configured": bool(os.environ.get("ANTHROPIC_API_KEY")),
206
+ }
207
+ except ImportError:
208
+ status["anthropic"] = {"installed": False, "configured": False}
209
+
210
+ # MiniMax (uses OpenAI SDK)
211
+ try:
212
+ import openai
213
+ status["minimax"] = {
214
+ "installed": True,
215
+ "configured": bool(os.environ.get("MINIMAX_API_KEY")),
216
+ }
217
+ except ImportError:
218
+ status["minimax"] = {"installed": False, "configured": False}
219
+
220
+ # Ollama
221
+ try:
222
+ import requests
223
+ host = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
224
+ try:
225
+ r = requests.get(f"{host}/api/tags", timeout=2)
226
+ status["ollama"] = {"installed": True, "configured": r.status_code == 200}
227
+ except:
228
+ status["ollama"] = {"installed": True, "configured": False}
229
+ except ImportError:
230
+ status["ollama"] = {"installed": False, "configured": False}
231
+
232
+ # Current provider
233
+ status["current_provider"] = os.environ.get("LLM_PROVIDER", "stub")
234
+ status["current_model"] = os.environ.get("LLM_MODEL", "(default)")
235
+
236
+ return status
cognitive/registry.py ADDED
@@ -0,0 +1,276 @@
1
+ """
2
+ Module Registry - Discover, manage, and install cognitive modules.
3
+
4
+ Search order:
5
+ 1. ./cognitive/modules (project-local)
6
+ 2. ~/.cognitive/modules (user-global)
7
+ 3. /usr/local/share/cognitive/modules (system-wide, optional)
8
+
9
+ Registry:
10
+ - cognitive-registry.json indexes public modules
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import shutil
16
+ import subprocess
17
+ import tempfile
18
+ from pathlib import Path
19
+ from typing import Optional
20
+ from urllib.request import urlopen
21
+ from urllib.error import URLError
22
+
23
+ # Standard module search paths
24
+ SEARCH_PATHS = [
25
+ Path.cwd() / "cognitive" / "modules", # Project-local
26
+ Path.home() / ".cognitive" / "modules", # User-global
27
+ Path("/usr/local/share/cognitive/modules"), # System-wide
28
+ ]
29
+
30
+ # User global install location
31
+ USER_MODULES_DIR = Path.home() / ".cognitive" / "modules"
32
+
33
+ # Default registry URL
34
+ DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/leizii/cognitive-modules/main/cognitive-registry.json"
35
+
36
+ # Local registry cache
37
+ REGISTRY_CACHE = Path.home() / ".cognitive" / "registry-cache.json"
38
+
39
+
40
+ def get_search_paths() -> list[Path]:
41
+ """Get all module search paths, including custom paths from env."""
42
+ paths = SEARCH_PATHS.copy()
43
+
44
+ # Add custom paths from environment
45
+ custom = os.environ.get("COGNITIVE_MODULES_PATH")
46
+ if custom:
47
+ for p in custom.split(":"):
48
+ paths.insert(0, Path(p))
49
+
50
+ return paths
51
+
52
+
53
+ def find_module(name: str) -> Optional[Path]:
54
+ """Find a module by name, searching all paths in order."""
55
+ for base_path in get_search_paths():
56
+ module_path = base_path / name
57
+ # Support both new and old format
58
+ if module_path.exists():
59
+ if (module_path / "MODULE.md").exists() or (module_path / "module.md").exists():
60
+ return module_path
61
+ return None
62
+
63
+
64
+ def list_modules() -> list[dict]:
65
+ """List all available modules from all search paths."""
66
+ modules = []
67
+ seen = set()
68
+
69
+ for base_path in get_search_paths():
70
+ if not base_path.exists():
71
+ continue
72
+
73
+ for module_dir in base_path.iterdir():
74
+ if not module_dir.is_dir():
75
+ continue
76
+ if module_dir.name in seen:
77
+ continue
78
+ # Support both formats
79
+ if not ((module_dir / "MODULE.md").exists() or (module_dir / "module.md").exists()):
80
+ continue
81
+
82
+ # Detect format
83
+ fmt = "new" if (module_dir / "MODULE.md").exists() else "old"
84
+
85
+ seen.add(module_dir.name)
86
+ modules.append({
87
+ "name": module_dir.name,
88
+ "path": module_dir,
89
+ "location": "local" if base_path == SEARCH_PATHS[0] else "global",
90
+ "format": fmt,
91
+ })
92
+
93
+ return modules
94
+
95
+
96
+ def ensure_user_modules_dir() -> Path:
97
+ """Ensure user global modules directory exists."""
98
+ USER_MODULES_DIR.mkdir(parents=True, exist_ok=True)
99
+ return USER_MODULES_DIR
100
+
101
+
102
+ def install_from_local(source: Path, name: Optional[str] = None) -> Path:
103
+ """Install a module from a local path."""
104
+ source = Path(source).resolve()
105
+ if not source.exists():
106
+ raise FileNotFoundError(f"Source not found: {source}")
107
+
108
+ # Check for valid module (either format)
109
+ if not ((source / "MODULE.md").exists() or (source / "module.md").exists()):
110
+ raise ValueError(f"Not a valid module (missing MODULE.md or module.md): {source}")
111
+
112
+ module_name = name or source.name
113
+ target = ensure_user_modules_dir() / module_name
114
+
115
+ if target.exists():
116
+ shutil.rmtree(target)
117
+
118
+ shutil.copytree(source, target)
119
+ return target
120
+
121
+
122
+ def install_from_git(url: str, subdir: Optional[str] = None, name: Optional[str] = None) -> Path:
123
+ """
124
+ Install a module from a git repository.
125
+
126
+ Supports:
127
+ - github:org/repo/path/to/module
128
+ - git+https://github.com/org/repo#subdir=path/to/module
129
+ - https://github.com/org/repo (with subdir parameter)
130
+ """
131
+ # Parse github: shorthand
132
+ if url.startswith("github:"):
133
+ parts = url[7:].split("/", 2)
134
+ if len(parts) < 2:
135
+ raise ValueError(f"Invalid github URL: {url}")
136
+ org, repo = parts[0], parts[1]
137
+ if len(parts) == 3:
138
+ subdir = parts[2]
139
+ url = f"https://github.com/{org}/{repo}.git"
140
+
141
+ # Parse git+https:// with fragment
142
+ elif url.startswith("git+"):
143
+ url = url[4:]
144
+ if "#" in url:
145
+ url, fragment = url.split("#", 1)
146
+ for part in fragment.split("&"):
147
+ if part.startswith("subdir="):
148
+ subdir = part[7:]
149
+
150
+ # Clone to temp directory
151
+ with tempfile.TemporaryDirectory() as tmpdir:
152
+ tmppath = Path(tmpdir)
153
+
154
+ # Shallow clone
155
+ result = subprocess.run(
156
+ ["git", "clone", "--depth", "1", url, str(tmppath / "repo")],
157
+ capture_output=True,
158
+ text=True
159
+ )
160
+ if result.returncode != 0:
161
+ raise RuntimeError(f"Git clone failed: {result.stderr}")
162
+
163
+ # Find module source
164
+ source = tmppath / "repo"
165
+ if subdir:
166
+ source = source / subdir
167
+
168
+ if not source.exists():
169
+ raise FileNotFoundError(f"Subdir not found: {subdir}")
170
+
171
+ # Determine module name
172
+ module_name = name or source.name
173
+
174
+ # Install from cloned source
175
+ return install_from_local(source, module_name)
176
+
177
+
178
+ def install_from_registry(module_name: str) -> Path:
179
+ """Install a module from the public registry."""
180
+ registry = fetch_registry()
181
+
182
+ if module_name not in registry.get("modules", {}):
183
+ raise ValueError(f"Module not found in registry: {module_name}")
184
+
185
+ module_info = registry["modules"][module_name]
186
+ source = module_info.get("source")
187
+
188
+ if not source:
189
+ raise ValueError(f"No source defined for module: {module_name}")
190
+
191
+ return install_module(source, name=module_name)
192
+
193
+
194
+ def install_module(source: str, name: Optional[str] = None) -> Path:
195
+ """
196
+ Install a module from various sources.
197
+
198
+ Sources:
199
+ - local:/path/to/module
200
+ - github:org/repo/path/to/module
201
+ - git+https://github.com/org/repo#subdir=path
202
+ - /absolute/path (treated as local)
203
+ - ./relative/path (treated as local)
204
+ - registry:module-name (from public registry)
205
+ """
206
+ if source.startswith("local:"):
207
+ return install_from_local(Path(source[6:]), name)
208
+ elif source.startswith("registry:"):
209
+ return install_from_registry(source[9:])
210
+ elif source.startswith("github:") or source.startswith("git+"):
211
+ return install_from_git(source, name=name)
212
+ elif source.startswith("/") or source.startswith("./") or source.startswith(".."):
213
+ return install_from_local(Path(source), name)
214
+ elif source.startswith("https://github.com"):
215
+ return install_from_git(source, name=name)
216
+ else:
217
+ # Try registry first, then local
218
+ try:
219
+ return install_from_registry(source)
220
+ except:
221
+ return install_from_local(Path(source), name)
222
+
223
+
224
+ def uninstall_module(name: str) -> bool:
225
+ """Uninstall a module from user global location."""
226
+ target = USER_MODULES_DIR / name
227
+ if target.exists():
228
+ shutil.rmtree(target)
229
+ return True
230
+ return False
231
+
232
+
233
+ def fetch_registry(url: Optional[str] = None, use_cache: bool = True) -> dict:
234
+ """Fetch the public module registry."""
235
+ url = url or os.environ.get("COGNITIVE_REGISTRY_URL", DEFAULT_REGISTRY_URL)
236
+
237
+ # Try cache first
238
+ if use_cache and REGISTRY_CACHE.exists():
239
+ try:
240
+ with open(REGISTRY_CACHE, 'r') as f:
241
+ return json.load(f)
242
+ except:
243
+ pass
244
+
245
+ # Fetch from URL
246
+ try:
247
+ with urlopen(url, timeout=5) as response:
248
+ data = json.loads(response.read().decode())
249
+
250
+ # Cache it
251
+ REGISTRY_CACHE.parent.mkdir(parents=True, exist_ok=True)
252
+ with open(REGISTRY_CACHE, 'w') as f:
253
+ json.dump(data, f)
254
+
255
+ return data
256
+ except (URLError, json.JSONDecodeError) as e:
257
+ # Return empty registry if fetch fails
258
+ return {"modules": {}, "error": str(e)}
259
+
260
+
261
+ def search_registry(query: str) -> list[dict]:
262
+ """Search the registry for modules matching query."""
263
+ registry = fetch_registry()
264
+ results = []
265
+
266
+ query_lower = query.lower()
267
+ for name, info in registry.get("modules", {}).items():
268
+ if query_lower in name.lower() or query_lower in info.get("description", "").lower():
269
+ results.append({
270
+ "name": name,
271
+ "description": info.get("description", ""),
272
+ "source": info.get("source", ""),
273
+ "version": info.get("version", ""),
274
+ })
275
+
276
+ return results
cognitive/runner.py ADDED
@@ -0,0 +1,140 @@
1
+ """
2
+ Module Runner - Execute cognitive modules with validation.
3
+ Supports both old and new module formats.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import jsonschema
11
+ import yaml
12
+
13
+ from .registry import find_module
14
+ from .loader import load_module
15
+ from .providers import call_llm
16
+
17
+
18
+ def validate_data(data: dict, schema: dict, label: str = "Data") -> list[str]:
19
+ """Validate data against schema. Returns list of errors."""
20
+ errors = []
21
+ if not schema:
22
+ return errors
23
+ try:
24
+ jsonschema.validate(instance=data, schema=schema)
25
+ except jsonschema.ValidationError as e:
26
+ errors.append(f"{label} validation error: {e.message} at {list(e.absolute_path)}")
27
+ except jsonschema.SchemaError as e:
28
+ errors.append(f"Schema error: {e.message}")
29
+ return errors
30
+
31
+
32
+ def substitute_arguments(text: str, input_data: dict) -> str:
33
+ """Substitute $ARGUMENTS and $N placeholders in text."""
34
+ # Get arguments
35
+ args_value = input_data.get("$ARGUMENTS", input_data.get("query", ""))
36
+
37
+ # Replace $ARGUMENTS
38
+ text = text.replace("$ARGUMENTS", str(args_value))
39
+
40
+ # Replace $ARGUMENTS[N] and $N for indexed access
41
+ if isinstance(args_value, str):
42
+ args_list = args_value.split()
43
+ for i, arg in enumerate(args_list):
44
+ text = text.replace(f"$ARGUMENTS[{i}]", arg)
45
+ text = text.replace(f"${i}", arg)
46
+
47
+ return text
48
+
49
+
50
+ def build_prompt(module: dict, input_data: dict) -> str:
51
+ """Build the complete prompt for the LLM."""
52
+ # Substitute $ARGUMENTS in prompt
53
+ prompt = substitute_arguments(module["prompt"], input_data)
54
+
55
+ parts = [
56
+ prompt,
57
+ "\n\n## Constraints\n",
58
+ yaml.dump(module["constraints"], default_flow_style=False),
59
+ "\n\n## Input\n",
60
+ "```json\n",
61
+ json.dumps(input_data, indent=2, ensure_ascii=False),
62
+ "\n```\n",
63
+ "\n## Instructions\n",
64
+ "Analyze the input and generate output matching the required schema.",
65
+ "Return ONLY valid JSON. Do not include any text before or after the JSON.",
66
+ ]
67
+ return "".join(parts)
68
+
69
+
70
+ def parse_llm_response(response: str) -> dict:
71
+ """Parse LLM response, handling potential markdown code blocks."""
72
+ text = response.strip()
73
+
74
+ # Remove markdown code blocks if present
75
+ if text.startswith("```"):
76
+ lines = text.split("\n")
77
+ start = 1
78
+ end = len(lines) - 1
79
+ for i, line in enumerate(lines[1:], 1):
80
+ if line.strip() == "```":
81
+ end = i
82
+ break
83
+ text = "\n".join(lines[start:end])
84
+
85
+ return json.loads(text)
86
+
87
+
88
+ def run_module(
89
+ name_or_path: str,
90
+ input_data: dict,
91
+ validate_input: bool = True,
92
+ validate_output: bool = True,
93
+ model: Optional[str] = None,
94
+ ) -> dict:
95
+ """
96
+ Run a cognitive module with the given input.
97
+ Supports both old and new module formats.
98
+
99
+ Args:
100
+ name_or_path: Module name or path to module directory
101
+ input_data: Input data dictionary
102
+ validate_input: Whether to validate input against schema
103
+ validate_output: Whether to validate output against schema
104
+ model: Optional model override
105
+
106
+ Returns:
107
+ The module output as a dictionary
108
+ """
109
+ # Find module path
110
+ path = Path(name_or_path)
111
+ if path.exists() and path.is_dir():
112
+ module_path = path
113
+ else:
114
+ module_path = find_module(name_or_path)
115
+ if not module_path:
116
+ raise FileNotFoundError(f"Module not found: {name_or_path}")
117
+
118
+ # Load module (auto-detects format)
119
+ module = load_module(module_path)
120
+
121
+ # Validate input
122
+ if validate_input and module["input_schema"]:
123
+ errors = validate_data(input_data, module["input_schema"], "Input")
124
+ if errors:
125
+ raise ValueError(f"Input validation failed: {errors}")
126
+
127
+ # Build prompt and call LLM
128
+ full_prompt = build_prompt(module, input_data)
129
+ response = call_llm(full_prompt, model=model)
130
+
131
+ # Parse response
132
+ output_data = parse_llm_response(response)
133
+
134
+ # Validate output
135
+ if validate_output and module["output_schema"]:
136
+ errors = validate_data(output_data, module["output_schema"], "Output")
137
+ if errors:
138
+ raise ValueError(f"Output validation failed: {errors}")
139
+
140
+ return output_data