feat(agent): v4.10.1 - config LLM dans l'UI (sélecteur provider/modèle, config runtime, admin + test connexion)
FlowDeck CI / test (push) Failing after 7s
FlowDeck CI / docker (push) Skipped

- Panneau agent : selects provider/modèles connus persistés par conversation
  (colonnes agent_conversations.provider/model, migration idempotente)
- GET /api/agent/providers enrichi (liste providers + modèles + requires_key)
- PATCH /api/agent/providers (admin) : config runtime DB-backed (table llm_config)
- POST /api/agent/providers/test : test connexion via LLMClient.ping() sans fallback mock
- Settings > Admin > Agent & IA : provider, modèle, clé API, URL API, save + test
- VERSION -> 4.10.1, CHANGELOG + ROADMAP mis à jour, 232 tests verts
This commit is contained in:
2026-09-05 10:58:09 -04:00
parent e752e46583
commit 3cb9edc1f3
11 changed files with 546 additions and 19 deletions
+92
View File
@@ -317,6 +317,98 @@ def test_router_tools_and_providers(client):
prov = client.get("/api/agent/providers").json()
assert prov["provider"] == "offline"
assert prov["available"] is False
assert "providers" in prov
def test_router_providers_enriched(client):
prov = client.get("/api/agent/providers").json()
assert "providers" in prov
ids = [p["id"] for p in prov["providers"]]
for expected in ("openai", "anthropic", "google", "ollama", "offline"):
assert expected in ids
openai = next(p for p in prov["providers"] if p["id"] == "openai")
assert openai["default_model"] == "gpt-4o"
assert openai["models"]
assert openai["requires_key"] is True
# offline requires no key and exposes no models
offline = next(p for p in prov["providers"] if p["id"] == "offline")
assert offline["requires_key"] is False
def test_router_patch_providers_updates_config(client):
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
admin = conn.execute("SELECT * FROM users WHERE login='admin'").fetchone()
sess = SessionManager.create_session(dict(admin))
client.cookies.set("flowdeck_session", sess)
r = client.patch("/api/agent/providers",
json={"provider": "openai", "model": "gpt-4o-mini",
"api_key": "sk-test", "api_base": ""})
assert r.status_code == 200
body = r.json()
assert body["provider"] == "openai"
assert body["model"] == "gpt-4o-mini"
assert body["has_api_key"] is True
# persisted in llm_config (single row id=1)
with get_conn() as conn:
row = conn.execute("SELECT * FROM llm_config WHERE id=1").fetchone()
assert row is not None
assert row["provider"] == "openai"
assert row["model"] == "gpt-4o-mini"
assert row["api_key"] == "sk-test"
# GET /providers reflects the new config
prov = client.get("/api/agent/providers").json()
assert prov["provider"] == "openai"
assert prov["model"] == "gpt-4o-mini"
assert prov["has_api_key"] is True
# a real provider with a key is marked available (no network call made)
assert prov["available"] is True
def test_router_patch_providers_requires_admin(client):
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
conn.execute("INSERT INTO users (login, full_name, is_admin) VALUES ('user_basic', 'Basic', 0)")
user = conn.execute("SELECT * FROM users WHERE login='user_basic'").fetchone()
conn.commit()
sess = SessionManager.create_session(dict(user))
client.cookies.set("flowdeck_session", sess)
r = client.patch("/api/agent/providers", json={"provider": "openai"})
assert r.status_code == 403
def test_router_providers_test_offline(client):
r = client.post("/api/agent/providers/test", json={"provider": "offline"})
assert r.status_code == 200
assert r.json()["ok"] is True
def test_router_providers_test_missing_key(client):
# openai without a key must report failure (no mock fallback for the ping)
r = client.post("/api/agent/providers/test",
json={"provider": "openai", "api_key": ""})
assert r.status_code == 200
assert r.json()["ok"] is False
assert "clé" in r.json()["error"].lower()
def test_router_run_persists_provider_model(client):
cid = _make_conversation(client)
resp = client.post(f"/api/agent/conversations/{cid}/run",
json={"message": "crée une collection Bilan",
"provider": "openai", "model": "gpt-4o-mini"})
assert resp.status_code == 200
detail = client.get(f"/api/agent/conversations/{cid}").json()
assert detail["conversation"]["provider"] == "openai"
assert detail["conversation"]["model"] == "gpt-4o-mini"
def _parse_sse(line: str) -> dict: