feat(ai): phase B function calling in-app (agent loop + endpoint /agent)
CI / lint (push) Successful in 57s
CI / security (push) Successful in 39s
CI / test (push) Successful in 1m13s
CI / build (push) Successful in 36s
CI / e2e (push) Successful in 10m13s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

- backend/ai_chat.py: chat_completion provider-agnostique (OpenAI-compat tools/tool_calls + Gemini functionDeclarations/functionCall), retry sans tools si rejete
- backend/agent/loop.py: run_agent multi-etapes (limite 10, truncation, confirmation two-step), LLM injectable
- endpoint opt-in POST /api/ai/bookslm/agent (events SSE tool/message/confirmation), extraction _resolve_system_prompt
- tests: test_ai_chat.py, test_agent_loop.py + 3 tests endpoint (728 passed au total)
- ROADMAP B1/B2/B3/B7 livres ; B4/B5/B6 restants
This commit is contained in:
2026-09-11 12:45:16 -04:00
parent 400224a089
commit 55696bfb31
9 changed files with 1056 additions and 42 deletions
+77
View File
@@ -746,3 +746,80 @@ class TestBooksLMChatModes:
)
assert resp.status_code in (200, 500), f"unexpected {resp.status_code}: {resp.text[:200]}"
assert resp.status_code != 422
# ── Tool-calling agent endpoint ───────────────────────────────────────
class TestBooksLMAgentEndpoint:
"""Tests for POST /api/ai/bookslm/agent (Phase B)."""
def test_agent_requires_auth(self, bookslm_client):
resp = bookslm_client.post(
"/api/ai/bookslm/agent",
json={"vault": "TestVault", "directory": "", "message": "Hello"},
)
assert resp.status_code == 401
def test_agent_tool_call_flow(self, bookslm_client, monkeypatch):
import backend.bookslm_routes as routes
from backend.ai_chat import LLMResponse, ToolCall
responses = [
LLMResponse(tool_calls=[ToolCall(id="1", name="list_directory", arguments={"vault": "TestVault"})]),
LLMResponse(content="Il y a des fichiers."),
]
async def fake_chat_completion(messages, **kwargs):
return responses.pop(0)
monkeypatch.setattr(routes, "chat_completion", fake_chat_completion)
monkeypatch.setattr(routes, "_resolve_provider_name", lambda requested: "deepseek")
token, _ = _login_bookslm(bookslm_client)
resp = bookslm_client.post(
"/api/ai/bookslm/agent",
json={"vault": "TestVault", "directory": "", "message": "liste les fichiers", "mode": "directory"},
headers={"Authorization": f"Bearer {token}"},
)
assert resp.status_code == 200
body = resp.text
assert "event: tool" in body
assert "list_directory" in body
assert "event: message" in body
assert "Il y a des fichiers." in body
def test_agent_pauses_for_confirmation(self, bookslm_client, monkeypatch):
import backend.bookslm_routes as routes
from backend.ai_chat import LLMResponse, ToolCall
from backend.tools import registry
from backend.tools.api import ToolRisk
from backend.tools.registry import ToolSpec
from backend.tools.schemas import ListVaultsInput
spec = ToolSpec(
name="_agent_write",
description="write for tests",
input_model=ListVaultsInput,
handler=lambda ctx, params: {"ok": True},
risk=ToolRisk.WRITE,
)
monkeypatch.setitem(registry._REGISTRY, "_agent_write", spec)
responses = [LLMResponse(tool_calls=[ToolCall(id="1", name="_agent_write", arguments={})])]
async def fake_chat_completion(messages, **kwargs):
return responses.pop(0)
monkeypatch.setattr(routes, "chat_completion", fake_chat_completion)
monkeypatch.setattr(routes, "_resolve_provider_name", lambda requested: "deepseek")
token, _ = _login_bookslm(bookslm_client)
resp = bookslm_client.post(
"/api/ai/bookslm/agent",
json={"vault": "TestVault", "directory": "", "message": "crée un fichier", "mode": "directory"},
headers={"Authorization": f"Bearer {token}"},
)
assert resp.status_code == 200
assert "event: confirmation" in resp.text
assert "_agent_write" in resp.text