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.
- cognitive/__init__.py +12 -0
- cognitive/cli.py +423 -0
- cognitive/loader.py +133 -0
- cognitive/providers/__init__.py +236 -0
- cognitive/registry.py +276 -0
- cognitive/runner.py +140 -0
- cognitive/subagent.py +245 -0
- cognitive/templates.py +186 -0
- cognitive/validator.py +302 -0
- cognitive_modules-0.1.0.dist-info/METADATA +295 -0
- cognitive_modules-0.1.0.dist-info/RECORD +15 -0
- cognitive_modules-0.1.0.dist-info/WHEEL +5 -0
- cognitive_modules-0.1.0.dist-info/entry_points.txt +2 -0
- cognitive_modules-0.1.0.dist-info/licenses/LICENSE +21 -0
- cognitive_modules-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|