diff --git a/coach/clients/perplexity_client.py b/coach/clients/perplexity_client.py index ceb5c27..5638a5c 100644 --- a/coach/clients/perplexity_client.py +++ b/coach/clients/perplexity_client.py @@ -56,15 +56,15 @@ class PerplexityClient: """ def __init__(self, api_key: str, model: str = SONAR, - timeout: int = 120): - if not api_key: - raise ValueError("Perplexity API key is required") + timeout: int = 120, base_url: str = None): + if not api_key and not base_url: + raise ValueError("Perplexity API key is required (or provide base_url for local)") self.api_key = api_key self.model = model self.timeout = timeout self._client = OpenAI( api_key=api_key, - base_url="https://api.perplexity.ai", + base_url=base_url or "https://api.perplexity.ai", timeout=timeout, ) diff --git a/coach/config.py b/coach/config.py index 79fe1aa..0f6baab 100644 --- a/coach/config.py +++ b/coach/config.py @@ -13,8 +13,8 @@ from pathlib import Path LAB_ROOT = Path(os.environ.get("COMMANDER_LAB_ROOT", Path(__file__).parent.parent)) # ── LM Studio (OpenAI-compatible API) ────────────────────── -LM_STUDIO_URL = os.environ.get("LM_STUDIO_URL", "http://192.168.0.241:1234/v1") -LM_STUDIO_MODEL = os.environ.get("LM_STUDIO_MODEL", "local-model") +LM_STUDIO_URL = os.environ.get("LM_STUDIO_URL", "http://localhost:11434/v1") +LM_STUDIO_MODEL = os.environ.get("LM_STUDIO_MODEL", "gpt-oss:20b") LM_STUDIO_TIMEOUT = int(os.environ.get("LM_STUDIO_TIMEOUT", "120")) # seconds LM_STUDIO_MAX_RETRIES = 3 @@ -41,9 +41,10 @@ DEFAULT_MAX_TOKENS = 8192 # ── Perplexity Deck Generation Settings ─────────────────────── # Provider for deck generation: "perplexity" (V3) or "local" (V2 collection-based) -DECK_GEN_PROVIDER = os.environ.get("DECK_GEN_PROVIDER", "perplexity") +DECK_GEN_PROVIDER = os.environ.get("DECK_GEN_PROVIDER", "local") # Model for deck generation (sonar = fast/$0.004, sonar-pro = deep/$0.04) -DECK_GEN_MODEL = os.environ.get("DECK_GEN_MODEL", "sonar") +DECK_GEN_MODEL = os.environ.get("DECK_GEN_MODEL", "gpt-oss:20b") +DECK_GEN_BASE_URL = os.environ.get("DECK_GEN_BASE_URL", "http://localhost:11434/v1") DECK_GEN_TEMPERATURE = float(os.environ.get("DECK_GEN_TEMPERATURE", "0.2")) DECK_GEN_MAX_TOKENS = int(os.environ.get("DECK_GEN_MAX_TOKENS", "16384")) @@ -53,7 +54,7 @@ SUBSTITUTION_MIN_SIMILARITY = float(os.environ.get("SUBSTITUTION_MIN_SIMILARITY" # Maximum number of alternatives to suggest per missing card SUBSTITUTION_MAX_ALTERNATIVES = int(os.environ.get("SUBSTITUTION_MAX_ALTERNATIVES", "5")) # Model for substitution fallback (sonar is fine — small focused queries) -SUBSTITUTION_MODEL = os.environ.get("SUBSTITUTION_MODEL", "sonar") +SUBSTITUTION_MODEL = os.environ.get("SUBSTITUTION_MODEL", "gpt-oss:20b") # Enable Perplexity fallback for low-confidence embedding matches SUBSTITUTION_USE_PPLX_FALLBACK = os.environ.get("SUBSTITUTION_USE_PPLX_FALLBACK", "true").lower() == "true" diff --git a/routes/coach.py b/routes/coach.py index 75f76fc..0e416a7 100644 --- a/routes/coach.py +++ b/routes/coach.py @@ -91,18 +91,24 @@ def init_coach_service(): log_coach.info(" Coach: Service initialized") - # Initialize V3 Deck Generator (Perplexity) + if CFG.pplx_api_key or os.environ.get('DECK_GEN_PROVIDER') == 'local': _deck_gen_v3_error = None - if CFG.pplx_api_key: try: from coach.clients.perplexity_client import PerplexityClient from coach.services.deck_generator import DeckGeneratorV3 - from coach.config import DECK_GEN_MODEL + from coach.config import DECK_GEN_MODEL, DECK_GEN_PROVIDER, DECK_GEN_BASE_URL - pplx_client = PerplexityClient( - api_key=CFG.pplx_api_key, - model=DECK_GEN_MODEL, - ) + if DECK_GEN_PROVIDER == 'local': + pplx_client = PerplexityClient( + api_key='ollama', + model=DECK_GEN_MODEL, + base_url=DECK_GEN_BASE_URL, + ) + else: + pplx_client = PerplexityClient( + api_key=CFG.pplx_api_key, + model=DECK_GEN_MODEL, + ) _deck_gen_v3 = DeckGeneratorV3( pplx_client=pplx_client, db_conn_factory=_get_db_conn, diff --git a/routes/deckgen.py b/routes/deckgen.py index c10b482..cbf96bd 100644 --- a/routes/deckgen.py +++ b/routes/deckgen.py @@ -771,26 +771,33 @@ async def _call_pplx_api(messages: list[dict], max_tokens: int = 4096, temperatu Uses httpx.AsyncClient so the uvicorn event loop is never stalled. Returns the assistant message content. """ - if not CFG.pplx_api_key: - raise HTTPException(400, 'Perplexity API key not configured. Set PPLX_API_KEY env var or --pplx-key.') + from coach.config import DECK_GEN_PROVIDER, DECK_GEN_BASE_URL, DECK_GEN_MODEL + + if DECK_GEN_PROVIDER == 'local': + base_url = DECK_GEN_BASE_URL + model = DECK_GEN_MODEL + headers = {'Content-Type': 'application/json'} + else: + if not CFG.pplx_api_key: + raise HTTPException(400, 'Perplexity API key not configured. Set PPLX_API_KEY env var or --pplx-key.') + base_url = 'https://api.perplexity.ai' + model = 'sonar' + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {CFG.pplx_api_key}', + } payload = { - 'model': 'sonar', + 'model': model, 'messages': messages, 'max_tokens': max_tokens, 'temperature': temperature, - 'return_related_questions': False, - } - headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {CFG.pplx_api_key}', } try: async with httpx.AsyncClient(timeout=120.0) as client: resp = await client.post( - 'https://api.perplexity.ai/chat/completions', + f'{base_url}/chat/completions', json=payload, headers=headers, - ) resp.raise_for_status() data = resp.json() except httpx.HTTPStatusError as e: