feat(api): seedgo compliance — json_handler, introspection, diagnostics (#69)
* fix(ai_mail): resolve relative mailbox_path bug + archive dead handlers Fix get_user_by_email() and get_all_users() returning relative mailbox_path values from registry instead of absolute paths (causing doubled paths in reply). Added path resolution matching get_current_user() pattern. Archive 7 unused handler files to apps/.archive/ (gitignored): - pending_work.py — planned feature, never wired - lock_utils.py — superseded by inbox_lock.py - data_ops.py — consumer (error_monitor) was archived - config_generator.py, users/load.py, registry/validate.py — scaffolded, never used - trigger/error_handler.py — superseded by trigger branch's event system Added test_user_paths.py (13 tests) covering absolute path resolution. Co-Authored-By: @ai_mail <ai_mail@aipass> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(spawn): seedgo compliance cleanup — 93% to 98% Archive legacy files (dev.local.md, ai_mail.local/, .seed/). Update META Modified dates on 22 files. Update README architecture tree and freshness. Fix 15 Pyright unused warnings. Add introspection no-args gates to 6 modules. Wire json_handler + log_operation to 15 operational files. Add seedgo bypass for 5 pure data files and log_structure. Remove deprecated dev.local.md from builder template. Co-Authored-By: @spawn <spawn@aipass> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(api): seedgo compliance — json_handler, introspection, diagnostics Wire json_handler.log_operation() into all 13 handler files. Add no-args introspection gates to 4 modules with standalone command routing. Add pyright directives for optional deps (Google, OpenAI) on 3 handler files. Co-Authored-By: @api <api@aipass> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: @ai_mail <ai_mail@aipass> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: @spawn <spawn@aipass> Co-authored-by: @api <api@aipass>
This commit is contained in:
co-authored by
@api
Claude Opus 4.6
@ai_mail
@spawn
parent
8c3ad2657c
commit
18cc8eb74f
@@ -29,6 +29,9 @@ from typing import Optional, Dict, List
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
|
||||
# ==============================================
|
||||
# CONSTANTS
|
||||
@@ -86,6 +89,7 @@ def read_env_file(env_var: str, search_paths: Optional[List[Path]] = None) -> Op
|
||||
key, value = line.split('=', 1)
|
||||
if key.strip() == env_var:
|
||||
# Found env_var in env_file
|
||||
json_handler.log_operation("env_loaded", {"variable": env_var, "source": str(env_file)})
|
||||
return value.strip()
|
||||
except Exception as e:
|
||||
# Error reading env_file
|
||||
|
||||
@@ -31,6 +31,9 @@ from typing import Optional, Dict, Any
|
||||
# Internal handlers
|
||||
from aipass.api.apps.handlers.auth.env import read_env_file
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
|
||||
# ==============================================
|
||||
# CONSTANTS
|
||||
@@ -86,23 +89,31 @@ def get_api_key(provider: str = "openrouter") -> Optional[str]:
|
||||
... print(f"Got key: {key[:20]}...")
|
||||
"""
|
||||
try:
|
||||
source = ""
|
||||
|
||||
# 1. Try config file
|
||||
key = get_key_from_config(provider)
|
||||
if key and validate_key(key, provider):
|
||||
# Using key from config
|
||||
return key
|
||||
source = "config"
|
||||
|
||||
# 2. Try environment variable
|
||||
key = get_key_from_env(provider)
|
||||
if key and validate_key(key, provider):
|
||||
# Using key from environment
|
||||
return key
|
||||
if not source:
|
||||
key = get_key_from_env(provider)
|
||||
if key and validate_key(key, provider):
|
||||
# Using key from environment
|
||||
source = "env"
|
||||
|
||||
# 3. Try .env file
|
||||
env_var = f"{provider.upper()}_API_KEY"
|
||||
key = read_env_file(env_var)
|
||||
if key and validate_key(key, provider):
|
||||
# Using key from .env file
|
||||
if not source:
|
||||
env_var = f"{provider.upper()}_API_KEY"
|
||||
key = read_env_file(env_var)
|
||||
if key and validate_key(key, provider):
|
||||
# Using key from .env file
|
||||
source = "dotenv"
|
||||
|
||||
if source:
|
||||
json_handler.log_operation("key_retrieved", {"provider": provider, "source": source})
|
||||
return key
|
||||
|
||||
# No valid key found
|
||||
|
||||
@@ -31,6 +31,9 @@ from typing import Dict, Any, Optional
|
||||
# Internal handlers
|
||||
from aipass.api.apps.handlers.json.json_handler import load_json, save_json
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
@@ -115,6 +118,7 @@ def load_provider_config(provider: str = "openrouter") -> Optional[Dict[str, Any
|
||||
|
||||
if provider_config:
|
||||
# Loaded config for provider
|
||||
json_handler.log_operation("config_loaded", {"provider": provider})
|
||||
return provider_config
|
||||
else:
|
||||
# Provider not found in config
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Created: 2026-03-14
|
||||
# Modified: 2026-03-14
|
||||
# =============================================
|
||||
# pyright: reportMissingImports=false, reportInvalidTypeForm=false, reportOptionalMemberAccess=false, reportOptionalCall=false
|
||||
|
||||
"""
|
||||
Google OAuth2 Authentication Handler
|
||||
@@ -22,6 +23,9 @@ Consumers get authenticated credentials, they decide what to do with them.
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
# =============================================
|
||||
# CONSTANTS
|
||||
# =============================================
|
||||
@@ -84,6 +88,7 @@ def load_credentials(scopes: Optional[list] = None) -> Optional["Credentials"]:
|
||||
|
||||
try:
|
||||
creds = Credentials.from_authorized_user_file(str(CREDS_PATH), effective_scopes)
|
||||
json_handler.log_operation("credentials_loaded", {"source": str(CREDS_PATH)})
|
||||
return creds
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -29,6 +29,9 @@ import ssl
|
||||
import time
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
|
||||
def is_ssl_error(exc: Exception) -> bool:
|
||||
"""Check if an exception is a transient SSL/connection error.
|
||||
@@ -81,6 +84,7 @@ def api_call_with_retry(
|
||||
except Exception as e:
|
||||
if attempt < max_retries and is_ssl_error(e):
|
||||
wait = 2 ** attempt
|
||||
json_handler.log_operation("api_retry_attempted", {"attempt": attempt + 1, "wait_seconds": wait})
|
||||
time.sleep(wait)
|
||||
if rebuild_service_fn:
|
||||
rebuild_service_fn()
|
||||
|
||||
@@ -27,6 +27,9 @@ from pathlib import Path
|
||||
import inspect
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
# =============================================
|
||||
# CONFIGURATION
|
||||
# =============================================
|
||||
@@ -61,11 +64,17 @@ def get_caller_info() -> Optional[Dict[str, Any]]:
|
||||
frame_path = Path(frame_info.filename)
|
||||
|
||||
if "flow" in frame_path.parts:
|
||||
return _detect_flow_caller(frame_path)
|
||||
result = _detect_flow_caller(frame_path)
|
||||
json_handler.log_operation("caller_detected", {"caller": result.get("caller_name"), "category": "flow"})
|
||||
return result
|
||||
elif "prax" in frame_path.parts:
|
||||
return _detect_prax_caller(frame_path)
|
||||
result = _detect_prax_caller(frame_path)
|
||||
json_handler.log_operation("caller_detected", {"caller": result.get("caller_name"), "category": "prax"})
|
||||
return result
|
||||
elif any("skills" in part for part in frame_path.parts):
|
||||
return _detect_skills_caller(frame_path)
|
||||
result = _detect_skills_caller(frame_path)
|
||||
json_handler.log_operation("caller_detected", {"caller": result.get("caller_name"), "category": "skills"})
|
||||
return result
|
||||
|
||||
# logger.info(f"[{MODULE_NAME}] Could not detect caller from stack trace")
|
||||
return None
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
# pyright: reportInvalidTypeForm=false, reportOptionalCall=false
|
||||
|
||||
"""
|
||||
OpenRouter Client Handler
|
||||
@@ -49,6 +50,7 @@ try:
|
||||
OPENAI_AVAILABLE = True
|
||||
except ImportError:
|
||||
# logger.error("OpenAI SDK not available. Install with: pip install openai")
|
||||
OpenAI = None # type: ignore[assignment,misc]
|
||||
OPENAI_AVAILABLE = False
|
||||
|
||||
# Handler imports
|
||||
@@ -56,6 +58,9 @@ from aipass.api.apps.handlers.auth.keys import get_api_key
|
||||
from aipass.api.apps.handlers.openrouter.caller import get_caller_info
|
||||
from aipass.api.apps.handlers.usage.tracking import track_usage
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
# =============================================
|
||||
# CONFIGURATION
|
||||
# =============================================
|
||||
@@ -116,6 +121,7 @@ def create_client(api_key: str, base_url: str = OPENROUTER_BASE_URL, timeout: in
|
||||
)
|
||||
|
||||
# logger.info(f"Created OpenRouter client - base_url: {base_url}, timeout: {timeout}s")
|
||||
json_handler.log_operation("client_initialized", {"base_url": base_url, "timeout": timeout})
|
||||
return client
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -34,6 +34,9 @@ import requests
|
||||
# Internal imports
|
||||
from aipass.api.apps.handlers.auth.keys import get_api_key
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
|
||||
# =============================================
|
||||
# CONSTANTS
|
||||
@@ -80,6 +83,7 @@ def get_available_models(api_key: Optional[str] = None) -> List[Dict]:
|
||||
|
||||
if models:
|
||||
# logger.info(f"[{MODULE_NAME}] Fetched {len(models)} models from OpenRouter")
|
||||
json_handler.log_operation("models_listed", {"count": len(models)})
|
||||
return models
|
||||
else:
|
||||
# logger.info(f"[{MODULE_NAME}] No models returned from API")
|
||||
|
||||
@@ -36,6 +36,9 @@ from aipass.prax import logger
|
||||
|
||||
from aipass.api.apps.handlers.openrouter.caller import detect_caller_from_stack
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
|
||||
# ===========================================
|
||||
# JSON UTILITIES
|
||||
@@ -223,6 +226,7 @@ def create_caller_config(caller: str, json_folder: Path) -> Dict[str, Any]:
|
||||
|
||||
logger.info(f"Auto-provisioned OpenRouter config for '{caller}'")
|
||||
logger.warning("Reload config and retry request")
|
||||
json_handler.log_operation("provider_provisioned", {"caller": caller})
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ from typing import Dict, Any, List, Optional
|
||||
# Standard library for JSON operations
|
||||
import json
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
|
||||
# =============================================
|
||||
# MODULE CONSTANTS
|
||||
@@ -222,6 +225,7 @@ def calculate_totals(usage_data: List[Dict]) -> Dict[str, float]:
|
||||
}
|
||||
|
||||
# logger.info(f"[{MODULE_NAME}] Calculated totals: {result['total_requests']} requests, ${result['total_cost']:.6f}")
|
||||
json_handler.log_operation("usage_aggregated", {"total_requests": result["total_requests"], "total_cost": result["total_cost"]})
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -25,6 +25,9 @@ from typing import Optional, Dict, List
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
|
||||
def _read_json(file_path: Path) -> Optional[Dict]:
|
||||
"""Read JSON file with error handling."""
|
||||
@@ -90,6 +93,7 @@ def cleanup_old_data(data_file_path: Path, retention_days: int = 30) -> int:
|
||||
_write_json(data_file_path, data)
|
||||
# logger.info(f"Cleaned up {len(old_generations)} generation entries")
|
||||
logger.info(f"Cleaned up {len(old_generations)} generation entries older than {retention_days} days")
|
||||
json_handler.log_operation("usage_cleanup", {"generations_removed": len(old_generations), "retention_days": retention_days})
|
||||
|
||||
return len(old_generations)
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ import time
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# JSON handler
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
|
||||
# =============================================
|
||||
# MODULE CONSTANTS
|
||||
# =============================================
|
||||
@@ -99,6 +102,7 @@ def track_usage(generation_id: str, caller: str, model: str = "unknown", api_key
|
||||
# Store the usage data
|
||||
if store_usage_data(caller, model, generation_id, metrics):
|
||||
# Successfully tracked usage
|
||||
json_handler.log_operation("usage_tracked", {"caller": caller, "model": model, "generation_id": generation_id})
|
||||
return {"success": True, "metrics": metrics}
|
||||
else:
|
||||
# Failed to store usage data
|
||||
|
||||
@@ -65,17 +65,32 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
if command not in ["get-key", "validate", "list-providers", "init"]:
|
||||
return False
|
||||
|
||||
# Help gate
|
||||
if args and args[0] in ("--help", "-h", "help"):
|
||||
print_help()
|
||||
return True
|
||||
|
||||
# Log operation
|
||||
json_handler.log_operation(f"api_key_{command}", {"command": command})
|
||||
|
||||
# Standalone commands — route before introspection gate
|
||||
if command == "list-providers":
|
||||
list_providers()
|
||||
return True
|
||||
if command == "init":
|
||||
init_env()
|
||||
return True
|
||||
|
||||
# NO-ARGS GATE (seedgo standard)
|
||||
if not args:
|
||||
print_introspection()
|
||||
return True
|
||||
|
||||
# Arg-required commands
|
||||
if command == "get-key":
|
||||
get_key(args)
|
||||
elif command == "validate":
|
||||
validate_key(args)
|
||||
elif command == "list-providers":
|
||||
list_providers()
|
||||
elif command == "init":
|
||||
init_env()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
|
||||
@@ -124,8 +124,20 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
Returns:
|
||||
True if command was handled, False to pass through.
|
||||
"""
|
||||
# Help gate
|
||||
if args and args[0] in ("--help", "-h", "help"):
|
||||
print_help()
|
||||
return True
|
||||
|
||||
# NO-ARGS GATE (seedgo standard)
|
||||
if not args:
|
||||
if command == "google":
|
||||
print_introspection()
|
||||
return True
|
||||
return False
|
||||
|
||||
# Only handle commands with "google" as the provider argument
|
||||
if not args or args[0] != "google":
|
||||
if args[0] != "google":
|
||||
return False
|
||||
|
||||
if command == "validate":
|
||||
|
||||
@@ -125,17 +125,33 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
if command not in ["test", "call", "models", "status"]:
|
||||
return False
|
||||
|
||||
# Help gate
|
||||
if args and args[0] in ("--help", "-h", "help"):
|
||||
print_help()
|
||||
return True
|
||||
|
||||
# Log operation
|
||||
json_handler.log_operation(f"openrouter_{command}", {"command": command})
|
||||
|
||||
# Standalone commands — route before introspection gate
|
||||
if command == "test":
|
||||
test_connection()
|
||||
elif command == "call":
|
||||
make_call(args)
|
||||
elif command == "models":
|
||||
return True
|
||||
if command == "models":
|
||||
list_models()
|
||||
elif command == "status":
|
||||
return True
|
||||
if command == "status":
|
||||
check_status()
|
||||
return True
|
||||
|
||||
# NO-ARGS GATE (seedgo standard)
|
||||
if not args:
|
||||
print_introspection()
|
||||
return True
|
||||
|
||||
# Arg-required commands
|
||||
if command == "call":
|
||||
make_call(args)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
|
||||
@@ -134,15 +134,30 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
if command not in ["track", "stats", "session", "caller-usage", "cleanup"]:
|
||||
return False
|
||||
|
||||
# Help gate
|
||||
if args and args[0] in ("--help", "-h", "help"):
|
||||
print_help()
|
||||
return True
|
||||
|
||||
# Log operation
|
||||
json_handler.log_operation(f"usage_{command}", {"command": command})
|
||||
|
||||
# Standalone commands — route before introspection gate
|
||||
if command == "stats":
|
||||
show_stats()
|
||||
return True
|
||||
if command == "session":
|
||||
show_session()
|
||||
return True
|
||||
|
||||
# NO-ARGS GATE (seedgo standard)
|
||||
if not args:
|
||||
print_introspection()
|
||||
return True
|
||||
|
||||
# Arg-required commands
|
||||
if command == "track":
|
||||
track_usage(args)
|
||||
elif command == "stats":
|
||||
show_stats()
|
||||
elif command == "session":
|
||||
show_session()
|
||||
elif command == "caller-usage":
|
||||
show_caller_usage(args)
|
||||
elif command == "cleanup":
|
||||
|
||||
Reference in New Issue
Block a user