personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'hopper-xopzv2m6-provider-status-api'

+246 -26
+5 -1
apps/settings/call.py
··· 260 260 def providers_show() -> None: 261 261 """Show provider configuration.""" 262 262 from think.models import TYPE_DEFAULTS 263 - from think.providers import get_provider_list 263 + from think.providers import build_provider_status, get_provider_list 264 264 265 265 config = _get_config() 266 266 providers_config = config.get("providers", {}) ··· 285 285 provider["name"]: auth_config.get(provider["name"], "platform") 286 286 for provider in providers_list 287 287 } 288 + vertex_creds_path = providers_config.get("vertex_credentials") 289 + vertex_creds_configured = bool(vertex_creds_path and Path(vertex_creds_path).exists()) 290 + provider_status = build_provider_status(providers_list, vertex_creds_configured) 288 291 result = { 289 292 "providers": providers_list, 293 + "provider_status": provider_status, 290 294 "generate": type_settings["generate"], 291 295 "cogitate": type_settings["cogitate"], 292 296 "api_keys": api_keys,
+6 -1
apps/settings/routes.py
··· 386 386 TYPE_DEFAULTS, 387 387 get_context_registry, 388 388 ) 389 - from think.providers import get_provider_list 389 + from think.providers import build_provider_status, get_provider_list 390 390 from think.talent import get_talent_configs 391 391 392 392 config = get_journal_config() ··· 470 470 except Exception: 471 471 pass 472 472 473 + provider_status = build_provider_status( 474 + providers_list, vertex_creds_configured 475 + ) 476 + 473 477 return jsonify( 474 478 { 475 479 "providers": providers_list, 480 + "provider_status": provider_status, 476 481 "generate": type_settings["generate"], 477 482 "cogitate": type_settings["cogitate"], 478 483 "contexts": contexts,
+58
apps/settings/tests/test_call.py
··· 96 96 assert payload["generate"]["provider"] == "google" 97 97 assert payload["cogitate"]["provider"] == "openai" 98 98 99 + def test_provider_status_key_set_cli_found(self, settings_env): 100 + """Provider with key set and CLI binary found.""" 101 + settings_env() 102 + 103 + with ( 104 + patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}), 105 + patch("shutil.which", return_value="/usr/bin/codex"), 106 + ): 107 + result = runner.invoke(call_app, ["settings", "providers", "show"]) 108 + 109 + assert result.exit_code == 0 110 + payload = json.loads(result.output) 111 + status = payload["provider_status"]["openai"] 112 + assert status["configured"] is True 113 + assert status["generate_ready"] is True 114 + assert status["cogitate_ready"] is True 115 + assert status["cogitate_cli"] == "codex" 116 + assert status["cogitate_cli_found"] is True 117 + assert status["issues"] == [] 118 + 119 + def test_provider_status_key_missing(self, settings_env): 120 + """Provider with key not set.""" 121 + settings_env() 122 + 123 + with ( 124 + patch.dict(os.environ, {}, clear=False), 125 + patch("shutil.which", return_value=None), 126 + ): 127 + os.environ.pop("OPENAI_API_KEY", None) 128 + result = runner.invoke(call_app, ["settings", "providers", "show"]) 129 + 130 + assert result.exit_code == 0 131 + payload = json.loads(result.output) 132 + status = payload["provider_status"]["openai"] 133 + assert status["configured"] is False 134 + assert status["generate_ready"] is False 135 + assert status["cogitate_ready"] is False 136 + assert "OPENAI_API_KEY not set" in status["issues"] 137 + 138 + def test_provider_status_key_set_cli_missing(self, settings_env): 139 + """Provider with key set but CLI binary not found.""" 140 + settings_env() 141 + 142 + with ( 143 + patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}), 144 + patch("shutil.which", return_value=None), 145 + ): 146 + result = runner.invoke(call_app, ["settings", "providers", "show"]) 147 + 148 + assert result.exit_code == 0 149 + payload = json.loads(result.output) 150 + status = payload["provider_status"]["anthropic"] 151 + assert status["configured"] is True 152 + assert status["generate_ready"] is True 153 + assert status["cogitate_ready"] is False 154 + assert status["cogitate_cli_found"] is False 155 + assert "claude CLI not found on PATH" in status["issues"] 156 + 99 157 100 158 class TestProvidersSetGenerate: 101 159 def test_set_generate_provider(self, settings_env):
+46
tests/baselines/api/settings/providers.json
··· 445 445 }, 446 446 "google_backend": "auto", 447 447 "key_validation": {}, 448 + "provider_status": { 449 + "anthropic": { 450 + "cogitate_cli": "claude", 451 + "cogitate_cli_found": false, 452 + "cogitate_ready": false, 453 + "configured": false, 454 + "generate_ready": false, 455 + "issues": [ 456 + "ANTHROPIC_API_KEY not set", 457 + "claude CLI not found on PATH" 458 + ] 459 + }, 460 + "google": { 461 + "cogitate_cli": "gemini", 462 + "cogitate_cli_found": false, 463 + "cogitate_ready": false, 464 + "configured": false, 465 + "generate_ready": false, 466 + "issues": [ 467 + "GOOGLE_API_KEY not set", 468 + "gemini CLI not found on PATH" 469 + ] 470 + }, 471 + "ollama": { 472 + "cogitate_cli": "opencode", 473 + "cogitate_cli_found": false, 474 + "cogitate_ready": false, 475 + "configured": false, 476 + "generate_ready": false, 477 + "issues": [ 478 + "Ollama not reachable at http://localhost:11434", 479 + "opencode CLI not found on PATH" 480 + ] 481 + }, 482 + "openai": { 483 + "cogitate_cli": "codex", 484 + "cogitate_cli_found": false, 485 + "cogitate_ready": false, 486 + "configured": false, 487 + "generate_ready": false, 488 + "issues": [ 489 + "OPENAI_API_KEY not set", 490 + "codex CLI not found on PATH" 491 + ] 492 + } 493 + }, 448 494 "providers": [ 449 495 { 450 496 "env_key": "",
+9 -4
tests/test_agents_check.py
··· 389 389 def test_cogitate_missing_binary_returns_skip(monkeypatch): 390 390 """_check_cogitate returns skip when CLI binary is not installed.""" 391 391 import think.agents as agents 392 + import think.providers as providers 392 393 393 - monkeypatch.setattr( 394 - "think.providers.PROVIDER_METADATA", 395 - {"fake": {"env_key": "FAKE_API_KEY", "label": "Fake Provider"}}, 394 + monkeypatch.setitem( 395 + providers.PROVIDER_METADATA, 396 + "fake", 397 + { 398 + "env_key": "FAKE_API_KEY", 399 + "label": "Fake Provider", 400 + "cogitate_cli": "nonexistent-binary-xyz", 401 + }, 396 402 ) 397 403 monkeypatch.setenv("FAKE_API_KEY", "test-key") 398 - monkeypatch.setattr(agents, "COGITATE_BINARIES", {"fake": "nonexistent-binary-xyz"}) 399 404 monkeypatch.setattr("shutil.which", lambda _: None) 400 405 401 406 status, msg = asyncio.run(agents._check_cogitate("fake", 2, 30))
+16 -1
tests/verify_api.py
··· 414 414 415 415 def walk(value: Any, key: str | None = None) -> Any: 416 416 if isinstance(value, dict): 417 - return { 417 + result = { 418 418 item_key: ( 419 419 0 420 420 if item_key in {"mtime", "created_at", "file_mtime"} ··· 433 433 ) 434 434 for item_key, item_value in value.items() 435 435 } 436 + if key == "provider_status": 437 + for _name, status in result.items(): 438 + if isinstance(status, dict) and "cogitate_cli" in status: 439 + status["cogitate_cli_found"] = False 440 + status["cogitate_ready"] = False 441 + cli = status.get("cogitate_cli", "") 442 + issues = [ 443 + i 444 + for i in status.get("issues", []) 445 + if "CLI not found" not in i 446 + ] 447 + if cli: 448 + issues.append(f"{cli} CLI not found on PATH") 449 + status["issues"] = sorted(issues) 450 + return result 436 451 437 452 if isinstance(value, list): 438 453 walked = [walk(item, key) for item in value]
+1 -10
think/agents.py
··· 55 55 # Minimum content length for transcript-based generation 56 56 MIN_INPUT_CHARS = 50 57 57 58 - # CLI binary names for cogitate interface, per provider 59 - COGITATE_BINARIES: dict[str, str] = { 60 - "google": "gemini", 61 - "openai": "codex", 62 - "anthropic": "claude", 63 - "ollama": "opencode", 64 - } 65 - 66 - 67 58 def setup_logging(verbose: bool = False) -> logging.Logger: 68 59 """Configure logging for agent CLI.""" 69 60 level = logging.DEBUG if verbose else logging.INFO ··· 1247 1238 return "skip", f"Ollama not reachable ({result.get('error', 'unreachable')})" 1248 1239 1249 1240 # Pre-flight: check cogitate CLI binary is installed 1250 - binary = COGITATE_BINARIES.get(provider_name) 1241 + binary = PROVIDER_METADATA[provider_name].get("cogitate_cli", "") 1251 1242 if binary and not shutil.which(binary): 1252 1243 return "skip", f"{binary} CLI not installed" 1253 1244
+105 -9
think/providers/__init__.py
··· 20 20 - ollama: Ollama local models 21 21 """ 22 22 23 + import os 24 + import shutil 23 25 from importlib import import_module 24 26 from types import ModuleType 25 27 from typing import Any, Dict, List ··· 44 46 # --------------------------------------------------------------------------- 45 47 # Provider Metadata 46 48 # --------------------------------------------------------------------------- 47 - # Display labels and environment variable names for each provider. 48 - # Used by settings UI to dynamically build provider dropdowns. 49 + # Display labels, environment variable names, and cogitate CLI binary names 50 + # for each provider. Used by settings UI, provider status, and agent health 51 + # checks. 49 52 # --------------------------------------------------------------------------- 50 53 51 54 PROVIDER_METADATA: Dict[str, Dict[str, Any]] = { 52 55 "google": { 53 56 "label": "Google (Gemini)", 54 57 "env_key": "GOOGLE_API_KEY", 58 + "cogitate_cli": "gemini", 55 59 "vertex_env_keys": [ 56 60 "GOOGLE_GENAI_USE_VERTEXAI", 57 61 "GOOGLE_APPLICATION_CREDENTIALS", 58 62 ], 59 63 }, 60 - "openai": {"label": "OpenAI (GPT)", "env_key": "OPENAI_API_KEY"}, 61 - "anthropic": {"label": "Anthropic (Claude)", "env_key": "ANTHROPIC_API_KEY"}, 62 - "ollama": {"label": "Ollama (Local)", "env_key": ""}, 64 + "openai": { 65 + "label": "OpenAI (GPT)", 66 + "env_key": "OPENAI_API_KEY", 67 + "cogitate_cli": "codex", 68 + }, 69 + "anthropic": { 70 + "label": "Anthropic (Claude)", 71 + "env_key": "ANTHROPIC_API_KEY", 72 + "cogitate_cli": "claude", 73 + }, 74 + "ollama": { 75 + "label": "Ollama (Local)", 76 + "env_key": "", 77 + "cogitate_cli": "opencode", 78 + }, 63 79 } 64 80 65 81 ··· 99 115 - label: Display label (e.g., "Google (Gemini)") 100 116 - env_key: Environment variable for API key 101 117 """ 102 - return [ 103 - {"name": name, **PROVIDER_METADATA.get(name, {"label": name, "env_key": ""})} 104 - for name in PROVIDER_REGISTRY 105 - ] 118 + providers = [] 119 + for name in PROVIDER_REGISTRY: 120 + meta = PROVIDER_METADATA.get(name, {"label": name, "env_key": ""}) 121 + provider = { 122 + "name": name, 123 + "label": meta.get("label", name), 124 + "env_key": meta.get("env_key", ""), 125 + } 126 + if "vertex_env_keys" in meta: 127 + provider["vertex_env_keys"] = meta["vertex_env_keys"] 128 + providers.append(provider) 129 + return providers 130 + 131 + 132 + def build_provider_status( 133 + providers_list: List[Dict[str, Any]], 134 + vertex_creds_configured: bool = False, 135 + ) -> Dict[str, Dict[str, Any]]: 136 + """Build per-provider readiness status. 137 + 138 + Parameters 139 + ---------- 140 + providers_list 141 + Output of get_provider_list(). 142 + vertex_creds_configured 143 + Whether Vertex AI credentials are configured (for Google). 144 + 145 + Returns 146 + ------- 147 + Dict[str, Dict[str, Any]] 148 + Keyed by provider name. Each entry has: configured, generate_ready, 149 + cogitate_ready, cogitate_cli, cogitate_cli_found, issues. 150 + """ 151 + status = {} 152 + for provider in providers_list: 153 + name = provider["name"] 154 + env_key = provider.get("env_key", "") 155 + meta = PROVIDER_METADATA.get(name, {}) 156 + cogitate_cli = meta.get("cogitate_cli", "") 157 + issues: list[str] = [] 158 + 159 + if name == "ollama": 160 + try: 161 + import httpx 162 + 163 + base_url = os.getenv( 164 + "OLLAMA_BASE_URL", "http://localhost:11434" 165 + ).rstrip("/") 166 + resp = httpx.get(f"{base_url}/api/version", timeout=2) 167 + resp.raise_for_status() 168 + configured = True 169 + except Exception: 170 + configured = False 171 + base_url = os.getenv( 172 + "OLLAMA_BASE_URL", "http://localhost:11434" 173 + ).rstrip("/") 174 + issues.append(f"Ollama not reachable at {base_url}") 175 + elif name == "google": 176 + has_key = bool(os.getenv(env_key)) 177 + configured = has_key or vertex_creds_configured 178 + if not configured: 179 + issues.append(f"{env_key} not set") 180 + else: 181 + configured = bool(os.getenv(env_key)) if env_key else False 182 + if not configured and env_key: 183 + issues.append(f"{env_key} not set") 184 + 185 + cogitate_cli_found = bool(shutil.which(cogitate_cli)) if cogitate_cli else False 186 + if cogitate_cli and not cogitate_cli_found: 187 + issues.append(f"{cogitate_cli} CLI not found on PATH") 188 + 189 + generate_ready = configured 190 + cogitate_ready = configured and cogitate_cli_found 191 + 192 + status[name] = { 193 + "configured": configured, 194 + "generate_ready": generate_ready, 195 + "cogitate_ready": cogitate_ready, 196 + "cogitate_cli": cogitate_cli, 197 + "cogitate_cli_found": cogitate_cli_found, 198 + "issues": issues, 199 + } 200 + return status 106 201 107 202 108 203 def get_provider_models(provider: str) -> list[dict]: ··· 156 251 "PROVIDER_METADATA", 157 252 "get_provider_module", 158 253 "get_provider_list", 254 + "build_provider_status", 159 255 "get_provider_models", 160 256 "validate_key", 161 257 ]