fix
This commit is contained in:
@@ -211,6 +211,141 @@ async def test_llm_endpoint(
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/llm/test-tools")
|
||||
async def test_llm_tools_endpoint(
|
||||
payload: Dict[str, Any] = Body(default={}),
|
||||
db: AsyncSession = Depends(get_db_dep),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
"""Probe whether the configured LLM endpoint supports OpenAI-style tool calls.
|
||||
|
||||
Sends a minimal chat completion request WITH a `tools` array containing one
|
||||
simple function (`get_time`). Returns:
|
||||
- ok: bool — did the model produce ANY well-formed response?
|
||||
- tool_calls_returned: bool — did the model emit at least one tool_call?
|
||||
- tool_call_name: str|null — the function name the model called (if any)
|
||||
- tool_call_args: dict|null — the parsed arguments (if any)
|
||||
- text: str — the model's text response (if any)
|
||||
- http_status: int — HTTP status of the chat-completions call
|
||||
- latency_ms: int
|
||||
- raw_tool_calls: list — the raw tool_calls array from the response
|
||||
- error: str|null — error message if the request failed
|
||||
- error_type: str|null
|
||||
|
||||
Use this to verify the model actually supports function-calling before
|
||||
relying on it for world-builder / orchestrator / step-writer flows.
|
||||
"""
|
||||
import time
|
||||
import httpx
|
||||
import json as _json
|
||||
|
||||
settings_map = await get_all_settings(db)
|
||||
overrides = (payload or {}).get("overrides") or {}
|
||||
for k, v in overrides.items():
|
||||
if k in EDITABLE_SETTING_KEYS:
|
||||
settings_map[k] = v
|
||||
|
||||
base_url = str(settings_map.get("llm.base_url", "")).rstrip("/")
|
||||
model = str(settings_map.get("llm.model", "local-model"))
|
||||
api_key = str(settings_map.get("llm.api_key", "dummy"))
|
||||
timeout_s = float(settings_map.get("llm.request_timeout", 30) or 30)
|
||||
|
||||
result: Dict[str, Any] = {
|
||||
"base_url": base_url,
|
||||
"model": model,
|
||||
"ok": False,
|
||||
"tool_calls_returned": False,
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key and api_key != "dummy":
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
# Minimal tool definition — the model should call this.
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_time",
|
||||
"description": "Returns the current time. Call this when the user asks for the time.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Optional timezone, e.g. 'UTC' or 'Europe/Moscow'",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
chat_url = f"{base_url}/chat/completions"
|
||||
chat_payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful assistant. When the user asks for the time, you MUST call the get_current_time tool."},
|
||||
{"role": "user", "content": "What time is it now? Use the get_current_time tool to find out."},
|
||||
],
|
||||
"tools": tools,
|
||||
"tool_choice": "auto",
|
||||
"max_tokens": 200,
|
||||
"temperature": 0.0,
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
started = time.monotonic()
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=timeout_s, write=10.0, pool=5.0)) as client:
|
||||
resp = await client.post(chat_url, json=chat_payload, headers=headers)
|
||||
result["http_status"] = resp.status_code
|
||||
result["latency_ms"] = int((time.monotonic() - started) * 1000)
|
||||
if resp.status_code >= 400:
|
||||
result["error"] = f"HTTP {resp.status_code}: {resp.text[:500]}"
|
||||
result["error_type"] = "HTTPError"
|
||||
return result
|
||||
data = resp.json()
|
||||
except httpx.ConnectError as e:
|
||||
cause = getattr(e, "__cause__", None) or getattr(e, "__context__", None)
|
||||
result["error"] = f"ConnectError: {e}"
|
||||
if cause:
|
||||
result["error"] += f" (cause: {cause})"
|
||||
result["error_type"] = "ConnectError"
|
||||
return result
|
||||
except Exception as e:
|
||||
result["error"] = f"{type(e).__name__}: {e}"
|
||||
result["error_type"] = type(e).__name__
|
||||
return result
|
||||
|
||||
try:
|
||||
choice = (data.get("choices") or [{}])[0]
|
||||
msg = choice.get("message", {})
|
||||
text = msg.get("content") or ""
|
||||
tool_calls = msg.get("tool_calls") or []
|
||||
result["text"] = text[:500]
|
||||
result["raw_tool_calls"] = tool_calls
|
||||
if tool_calls:
|
||||
result["tool_calls_returned"] = True
|
||||
first = tool_calls[0]
|
||||
fn = first.get("function", {}) if isinstance(first, dict) else {}
|
||||
result["tool_call_name"] = fn.get("name")
|
||||
args_str = fn.get("arguments", "{}")
|
||||
try:
|
||||
result["tool_call_args"] = _json.loads(args_str) if args_str else {}
|
||||
except _json.JSONDecodeError:
|
||||
result["tool_call_args"] = {"_raw": args_str}
|
||||
result["ok"] = True
|
||||
result["usage"] = data.get("usage", {})
|
||||
except Exception as e:
|
||||
result["error"] = f"response_parse_failed: {type(e).__name__}: {e}"
|
||||
result["error_type"] = type(e).__name__
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/llm-logs", response_model=List[LlmLogOut])
|
||||
async def list_llm_logs(
|
||||
limit: int = 50,
|
||||
|
||||
Reference in New Issue
Block a user