feat(agent): v4.12.0 - refonte config 'Agent & IA', fournisseurs actifs/testes dans le panneau, contexte document + guide d'utilisation
This commit is contained in:
+126
-13
@@ -15,11 +15,12 @@ from app.db import get_conn
|
||||
from app.config import settings
|
||||
from app.auth.session import get_current_user
|
||||
from app.services.agent_engine import AgentEngine, undo_action
|
||||
from app.services.llm_client import LLMClient, PROVIDERS
|
||||
from app.services.llm_client import LLMClient, PROVIDERS, PROVIDER_MODELS
|
||||
from app.services.llm_config import (
|
||||
get_llm_config, set_llm_config, provider_info,
|
||||
get_user_llm_key, list_user_llm_keys, upsert_user_llm_key,
|
||||
delete_user_llm_key, fetch_provider_models,
|
||||
mark_llm_config_verified, mark_user_llm_key_verified,
|
||||
)
|
||||
from app.services.tool_registry import ToolRegistry
|
||||
from app.services.permission_manager import PermissionManager
|
||||
@@ -298,6 +299,7 @@ async def run_conversation(request: Request, conversation_id: int):
|
||||
mentions=body.get("mentions"),
|
||||
files=body.get("files"),
|
||||
skill_id=body.get("skill_id"),
|
||||
extra_context=body.get("context"),
|
||||
):
|
||||
yield f"data: {json.dumps(ev, ensure_ascii=False)}\n\n"
|
||||
|
||||
@@ -427,12 +429,26 @@ async def list_tools(request: Request):
|
||||
|
||||
@router.get("/providers")
|
||||
async def list_providers(request: Request):
|
||||
"""Expose the LLM providers list + current config to the UI."""
|
||||
"""Expose the LLM providers list + current config to the UI.
|
||||
|
||||
Every provider known by FlowDeck is returned (the Settings UI lets the user
|
||||
configure any of them). Each provider also carries activation flags used by
|
||||
the Agent panel to offer only the providers that are *configured and
|
||||
functional* for the current user:
|
||||
|
||||
- ``configured`` : a usable credential exists (own key OR the workspace
|
||||
default provider carries a key, OR no key is required).
|
||||
- ``verified`` : the last connection test / model fetch succeeded.
|
||||
- ``functional`` : the provider is ready to chat (verified, or `offline`).
|
||||
"""
|
||||
user_id = await _current_user_id(request)
|
||||
llm = LLMClient()
|
||||
cfg = get_llm_config()
|
||||
keys = list_user_llm_keys(user_id)
|
||||
keys_map = {k["provider"]: k for k in keys}
|
||||
default_provider = (cfg.get("provider") or "offline").lower()
|
||||
global_has_key = bool(cfg.get("api_key")) and default_provider != "offline"
|
||||
global_verified = bool(cfg.get("verified")) and global_has_key
|
||||
provs = provider_info()
|
||||
for p in provs:
|
||||
k = keys_map.get(p["id"])
|
||||
@@ -440,7 +456,28 @@ async def list_providers(request: Request):
|
||||
for m in k["models"]:
|
||||
if m not in p["models"]:
|
||||
p["models"].append(m)
|
||||
p["has_key"] = bool(k and k["has_key"])
|
||||
user_has_key = bool(k and k["has_key"])
|
||||
is_default = p["id"] == default_provider
|
||||
configured = bool(k and k["has_key"]) or (is_default and global_has_key) \
|
||||
or not p["requires_key"]
|
||||
user_verified = bool(k and k.get("verified"))
|
||||
verified = (user_verified and bool(k and k["has_key"])) or \
|
||||
(is_default and global_has_key and global_verified)
|
||||
if not p["requires_key"] and not (k and k["has_key"]) and not (is_default and global_has_key):
|
||||
# ollama / offline: no key needed, but only offline is usable as-is;
|
||||
# local providers must still pass a connection test.
|
||||
verified = bool(user_verified or (is_default and global_verified)) \
|
||||
if p["id"] != "offline" else True
|
||||
p["has_key"] = user_has_key
|
||||
p["configured"] = bool(configured)
|
||||
p["source"] = ("user" if user_has_key else
|
||||
"global" if is_default and global_has_key else "open")
|
||||
p["verified"] = bool(verified)
|
||||
p["verified_model"] = (k.get("verified_model") if user_has_key and k else
|
||||
cfg.get("verified_model") if is_default and global_has_key else "") or ""
|
||||
p["last_error"] = (k.get("last_error") if user_has_key and k else
|
||||
cfg.get("last_error") if is_default and global_has_key else "") or ""
|
||||
p["functional"] = p["id"] == "offline" or bool(verified)
|
||||
return {
|
||||
"provider": llm.provider,
|
||||
"model": llm.default_model,
|
||||
@@ -448,6 +485,8 @@ async def list_providers(request: Request):
|
||||
"max_iterations": settings.agent_max_iterations,
|
||||
"api_base": cfg["api_base"],
|
||||
"has_api_key": bool(cfg["api_key"]),
|
||||
"default_provider": default_provider,
|
||||
"default_verified": bool(cfg.get("verified")),
|
||||
"providers": provs,
|
||||
"keys": keys,
|
||||
}
|
||||
@@ -494,10 +533,68 @@ async def delete_llm_key(request: Request, llm_provider: str):
|
||||
return {"status": "deleted", "provider": provider}
|
||||
|
||||
|
||||
@router.post("/keys/{llm_provider}/test")
|
||||
async def test_user_llm_key(request: Request, llm_provider: str):
|
||||
"""Non-admin: verify one of the user's own provider keys (no mock fallback).
|
||||
|
||||
On success the provider is flagged ``verified`` so it can be offered in the
|
||||
Agent panel; on failure the stored error is kept for display in Settings.
|
||||
"""
|
||||
user_id = await _current_user_id(request)
|
||||
provider = llm_provider.lower()
|
||||
if provider not in PROVIDERS:
|
||||
raise HTTPException(status_code=400, detail=f"Provider inconnu: {provider}")
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
stored = get_user_llm_key(user_id, provider)
|
||||
api_key = (body.get("api_key") or "").strip()
|
||||
api_base = (body.get("api_base") or "").strip()
|
||||
if not api_key and stored and stored.get("api_key"):
|
||||
api_key = stored["api_key"]
|
||||
if not api_base:
|
||||
api_base = (stored.get("api_base") or "").strip() if stored else ""
|
||||
_, provider_default = PROVIDERS.get(provider, (None, "gpt-4o"))
|
||||
candidates = PROVIDER_MODELS.get(provider) or [provider_default]
|
||||
model = (body.get("model") or "").strip() or \
|
||||
((stored or {}).get("default_model") or "") or \
|
||||
next((m for m in candidates if m), provider_default)
|
||||
# Only record the result against the *stored* credential when it is the one
|
||||
# being tested (avoids flagging a stored key from a test run on a typed key).
|
||||
can_record = bool(stored) and (
|
||||
not (stored or {}).get("api_key") or
|
||||
api_key == (stored or {}).get("api_key")
|
||||
)
|
||||
llm = LLMClient(provider=provider, api_key=api_key, api_base=api_base or None)
|
||||
try:
|
||||
resp = await llm.ping(model=model or None)
|
||||
except Exception as exc: # noqa: BLE001 — surface connectivity errors
|
||||
if can_record:
|
||||
mark_user_llm_key_verified(user_id, provider, False, error=str(exc))
|
||||
return {"ok": False, "provider": provider, "error": str(exc), "verified": False}
|
||||
if not stored and provider in ("ollama", "offline"):
|
||||
stored = upsert_user_llm_key(user_id, provider, api_key="", api_base=api_base)
|
||||
can_record = True
|
||||
if can_record:
|
||||
mark_user_llm_key_verified(
|
||||
user_id, provider, True,
|
||||
model=resp.model or model or "",
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"provider": provider,
|
||||
"model": resp.model or model or "",
|
||||
"reply": (resp.text or "").strip()[:200],
|
||||
"verified": bool(can_record),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/keys/{llm_provider}/models")
|
||||
async def fetch_llm_models(request: Request, llm_provider: str):
|
||||
"""Fetch the live model list from a provider. Falls back to the user's
|
||||
stored key when no key is supplied in the body."""
|
||||
stored key when no key is supplied in the body.
|
||||
|
||||
A successful fetch proves connectivity, so when it used the *stored* key the
|
||||
provider is flagged ``verified`` (functional) for the Agent panel.
|
||||
"""
|
||||
user_id = await _current_user_id(request)
|
||||
provider = llm_provider.lower()
|
||||
if provider not in PROVIDERS:
|
||||
@@ -505,13 +602,18 @@ async def fetch_llm_models(request: Request, llm_provider: str):
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
api_key = (body.get("api_key") or "").strip()
|
||||
api_base = (body.get("api_base") or "").strip()
|
||||
used_stored = False
|
||||
if not api_key:
|
||||
stored = get_user_llm_key(user_id, provider)
|
||||
if stored and stored.get("api_key"):
|
||||
api_key = stored["api_key"]
|
||||
api_base = api_base or (stored.get("api_base") or "").strip()
|
||||
used_stored = True
|
||||
try:
|
||||
models = await fetch_provider_models(provider, api_key=api_key, api_base=api_base)
|
||||
if used_stored and models:
|
||||
mark_user_llm_key_verified(user_id, provider, True,
|
||||
model=(body.get("default_model") or "").strip())
|
||||
return {"ok": True, "provider": provider, "models": models}
|
||||
except Exception as exc: # noqa: BLE001 — surface connectivity errors
|
||||
return {"ok": False, "provider": provider, "error": str(exc)}
|
||||
@@ -543,26 +645,37 @@ async def update_provider_config(request: Request):
|
||||
|
||||
@router.post("/providers/test")
|
||||
async def test_provider_config(request: Request):
|
||||
"""Admin: verify a provider is reachable (no mock fallback)."""
|
||||
"""Admin: verify a provider is reachable (no mock fallback).
|
||||
|
||||
A successful test flags the workspace default provider as ``verified`` so it
|
||||
becomes available (functional) for every user in the Agent panel.
|
||||
"""
|
||||
await _current_admin(request)
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
provider = (body.get("provider") or "").strip().lower()
|
||||
provider = (body.get("provider") or "").strip().lower() or None
|
||||
if provider and provider not in PROVIDERS:
|
||||
raise HTTPException(status_code=400, detail=f"Provider inconnu: {provider}")
|
||||
llm = LLMClient(
|
||||
provider=provider or None,
|
||||
provider=provider,
|
||||
api_key=body.get("api_key"),
|
||||
api_base=(body.get("api_base") or "").strip() or None,
|
||||
)
|
||||
try:
|
||||
resp = await llm.ping(model=(body.get("model") or "").strip() or None)
|
||||
return {
|
||||
"ok": True,
|
||||
"model": resp.model or (body.get("model") or "").strip() or llm.default_model,
|
||||
"reply": (resp.text or "").strip()[:200],
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001 — surface real connectivity errors
|
||||
return {"ok": False, "error": str(exc)}
|
||||
cfg = get_llm_config()
|
||||
if (provider or cfg.get("provider")) == cfg.get("provider"):
|
||||
mark_llm_config_verified(False, error=str(exc))
|
||||
return {"ok": False, "error": str(exc), "verified": False}
|
||||
cfg = get_llm_config()
|
||||
if (provider or cfg.get("provider")) == cfg.get("provider"):
|
||||
mark_llm_config_verified(True, model=resp.model or llm.default_model or "")
|
||||
return {
|
||||
"ok": True,
|
||||
"model": resp.model or (body.get("model") or "").strip() or llm.default_model,
|
||||
"reply": (resp.text or "").strip()[:200],
|
||||
"verified": True,
|
||||
}
|
||||
|
||||
|
||||
# ── Agents by id (registered LAST so static routes /tools, /skills, … win) ──
|
||||
|
||||
Reference in New Issue
Block a user