feat: LLM hot-swap voice command + Ollama backend (148 → 151 commands)

Closes the last functional parity gap with rust. Python now has voice-driven
Ollama↔Groq switching, persistent across restarts.

llm_backend.py (new, ~130 lines)
  Singleton module:
    current_client()     → active OpenAI client (or None)
    current_backend()    → 'groq' | 'ollama' | 'none'
    current_model()      → active model name
    swap_to(name)        → hot-swap + persist to <here>/llm_backend.txt
    parse_backend(name)  → ru/en alias normaliser ('облако'→groq, 'локальный'→ollama)

  Backends:
    - Groq:   config.GROQ_TOKEN + GROQ_BASE_URL + GROQ_MODEL (defaults)
    - Ollama: OLLAMA_BASE_URL (default http://localhost:11434/v1) +
              OLLAMA_MODEL (default qwen2.5:3b). api_key='ollama' (placeholder,
              openai lib insists on a non-empty string; Ollama ignores it).

  Init precedence (idempotent _ensure_init):
    1. Persisted choice from llm_backend.txt
    2. JARVIS_LLM env var
    3. Auto-detect: Groq if token present, else Ollama

Voice commands (commands.yaml, +3 entries → 151)
  llm_switch_local  → ollama
  llm_switch_cloud  → groq
  llm_status        → speaks current backend

extensions.py
  + do_llm_switch / do_llm_status handlers
  + llm_backend import
  do_interesting_fact migrated off direct OpenAI(...) to use llm_backend.current_*

dev_handlers.py
  + llm_backend import
  do_codebase_ask + do_github_summarize_pr migrated to llm_backend.current_*

vision_handler.py
  Vision call documented as Groq-specific (Ollama doesn't expose vision via
  OpenAI-compat in our stack), kept direct config.GROQ_TOKEN reading.

Tests: ast.parse passes for all 5 modules. yaml.safe_load = 151 entries.

PYTHON PARITY VS RUST IS NOW FUNCTIONALLY COMPLETE.
Only remaining rust-only feature: the Tauri GUI (python is console-only).
This commit is contained in:
Bossiara13 2026-05-16 00:44:42 +03:00
parent 31ff997782
commit 8d3da4ea06
6 changed files with 254 additions and 53 deletions

View file

@ -15,6 +15,7 @@ import re
import subprocess
import memory_store
import llm_backend
_speak_fn = print
@ -146,25 +147,11 @@ def do_codebase_ask(action, voice):
digest = "\n\n".join(f"--- {p} ---\n{c}" for p, c in chunks)
try:
import config as cfg
except ImportError:
cfg = None
token = getattr(cfg, 'GROQ_TOKEN', None) if cfg else None
if not token:
_speak("LLM не настроен — нужен GROQ_TOKEN.")
client = llm_backend.current_client()
if client is None:
_speak("LLM не настроен.")
return
try:
from openai import OpenAI
except ImportError:
_speak("OpenAI клиент не установлен.")
return
client = OpenAI(
api_key=token,
base_url=getattr(cfg, 'GROQ_BASE_URL', 'https://api.groq.com/openai/v1'),
)
model = llm_backend.current_model()
user_prompt = (
f"Ты — старший разработчик. По digest проекта ответь на вопрос пользователя "
@ -174,7 +161,7 @@ def do_codebase_ask(action, voice):
try:
resp = client.chat.completions.create(
model=getattr(cfg, 'GROQ_MODEL', 'llama-3.3-70b-versatile'),
model=model,
messages=[
{'role': 'system', 'content': 'Ты — внимательный код-ревьюер. По существу, без воды.'},
{'role': 'user', 'content': user_prompt},
@ -291,25 +278,11 @@ def do_github_summarize_pr(action, voice):
files = pr.get('changedFiles', '?')
author = (pr.get('author') or {}).get('login', '(unknown)')
try:
import config as cfg
except ImportError:
cfg = None
token = getattr(cfg, 'GROQ_TOKEN', None) if cfg else None
if not token:
_speak(f"PR номер {number}: {title}. Без LLM сводки — нужен GROQ_TOKEN.")
client = llm_backend.current_client()
if client is None:
_speak(f"PR номер {number}: {title}. Без LLM — настройте Groq или Ollama.")
return
try:
from openai import OpenAI
except ImportError:
_speak("OpenAI клиент не установлен.")
return
client = OpenAI(
api_key=token,
base_url=getattr(cfg, 'GROQ_BASE_URL', 'https://api.groq.com/openai/v1'),
)
model = llm_backend.current_model()
user_prompt = (
f"Repo: {repo}\nPR #{number}: {title}\nAuthor: {author}\n"
@ -321,7 +294,7 @@ def do_github_summarize_pr(action, voice):
try:
resp = client.chat.completions.create(
model=getattr(cfg, 'GROQ_MODEL', 'llama-3.3-70b-versatile'),
model=model,
messages=[
{'role': 'system', 'content': "Ты — старший разработчик, делаешь код-ревью. По делу, без воды."},
{'role': 'user', 'content': user_prompt},