feat(translate): language detection, async HTTP LLM, history model, agent improvements

- Add async HTTP-based LLM transport (_llm_async_http.py)
- Add orthogonal LLM call tests
- Improve language detection (_lang_detect.py) and batch insert
- Update translate schemas, service utils, preview constants/prompts
- Add TranslateHistoryModel with pagination and filtering
- Update agent confirmation, persistence, langgraph setup, run, tools
- Improve LLM health checking in shared module
- Update translate runs API, history route
This commit is contained in:
2026-07-08 19:35:49 +03:00
parent 97a64ce056
commit c488a63dc0
30 changed files with 1148 additions and 550 deletions

View File

@@ -15,6 +15,7 @@ from typing import Any
from langchain_openai import ChatOpenAI
from ss_tools.agent._llm_params import chat_openai_kwargs
from ss_tools.shared._llm_http import get_shared_http_client
from ss_tools.agent._tool_resolver import (
extract_tool_call_from_state,
find_tool,
@@ -287,12 +288,15 @@ async def _format_tool_output_via_llm(
config = await _fetch_llm_config()
if config and config.get("configured"):
try:
llm = ChatOpenAI(**chat_openai_kwargs(
model=config.get("default_model", "gpt-4o-mini"),
base_url=config.get("base_url", "https://api.openai.com/v1"),
api_key=config["api_key"],
max_tokens=1024,
))
llm = ChatOpenAI(
http_client=get_shared_http_client(),
**chat_openai_kwargs(
model=config.get("default_model", "gpt-4o-mini"),
base_url=config.get("base_url", "https://api.openai.com/v1"),
api_key=config["api_key"],
max_tokens=1024,
),
)
prompt = (
f"Tool '{tool_name}' returned this data:\n\n{text}\n\n"
"Summarize this data in a concise, human-readable format. "

View File

@@ -14,10 +14,9 @@ import re
from typing import Any
import uuid
import httpx
from ss_tools.agent._config import AGENT_PREFETCH_DASHBOARD_LIMIT as _PREFETCH_LIMIT, FASTAPI_URL, SERVICE_JWT as _SERVICE_JWT
from ss_tools.agent._llm_params import add_temperature_if_supported
from ss_tools.shared._llm_http import get_shared_http_client
from ss_tools.shared.logger import logger
SAVE_API_URL = FASTAPI_URL + "/api/agent/conversations/save"
@@ -119,10 +118,10 @@ async def _get_llm_config() -> dict[str, Any] | None:
headers = {"Content-Type": "application/json"}
if service_token:
headers["Authorization"] = f"Bearer {service_token}"
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(f"{fastapi_url}/api/agent/llm-config", headers=headers)
if resp.status_code == 200:
return resp.json()
client = get_shared_http_client(timeout=10)
resp = await client.get(f"{fastapi_url}/api/agent/llm-config", headers=headers)
if resp.status_code == 200:
return resp.json()
except Exception:
pass
return None
@@ -152,10 +151,10 @@ async def _call_llm_for_title(user_text: str) -> str | None:
if base.endswith("/v1"):
base = base[:-3]
api_url = base + "/v1/chat/completions"
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(api_url, json=payload, headers=headers)
if resp.status_code != 200:
return None
client = get_shared_http_client(timeout=180)
resp = await client.post(api_url, json=payload, headers=headers)
if resp.status_code != 200:
return None
data = resp.json()
title = data.get("choices", [{}])[0].get("message", {}).get("content", "")
if title:
@@ -188,8 +187,8 @@ async def generate_llm_title(conv_id: str, user_text: str) -> None:
if _SERVICE_JWT:
headers["Authorization"] = f"Bearer {_SERVICE_JWT}"
payload = {"conversation_id": conv_id, "title": title, "user_id": "admin", "messages": []}
async with httpx.AsyncClient(timeout=5) as client:
await client.post(SAVE_API_URL, json=payload, headers=headers)
client = get_shared_http_client(timeout=10)
await client.post(SAVE_API_URL, json=payload, headers=headers)
logger.reflect("LLM title updated", payload={"conv_id": conv_id, "title": title[:40]}, extra={"src": "AgentChat.Persistence"})
except Exception as e:
logger.explore("LLM title save failed", payload={"conv_id": conv_id}, error=str(e), extra={"src": "AgentChat.Persistence"})
@@ -204,14 +203,14 @@ async def generate_llm_title(conv_id: str, user_text: str) -> None:
async def prefetch_dashboards(env_id: str) -> str:
try:
from ss_tools.agent.tools import FASTAPI_URL, _dual_auth_headers
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
f"{FASTAPI_URL}/api/dashboards",
params={"q": "", "env_id": env_id or ""},
headers=_dual_auth_headers(),
)
if resp.status_code != 200:
return ""
client = get_shared_http_client(timeout=10)
resp = await client.get(
f"{FASTAPI_URL}/api/dashboards",
params={"q": "", "env_id": env_id or ""},
headers=_dual_auth_headers(),
)
if resp.status_code != 200:
return ""
data = resp.json()
dashboards = data.get("dashboards", [])
if not dashboards:
@@ -243,14 +242,14 @@ async def prefetch_dashboards(env_id: str) -> str:
async def prefetch_databases(env_id: str) -> str:
try:
from ss_tools.agent.tools import FASTAPI_URL, _dual_auth_headers
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
f"{FASTAPI_URL}/api/agent/superset/databases",
params={"environment_id": env_id or ""},
headers=_dual_auth_headers(),
)
if resp.status_code != 200:
return ""
client = get_shared_http_client(timeout=10)
resp = await client.get(
f"{FASTAPI_URL}/api/agent/superset/databases",
params={"environment_id": env_id or ""},
headers=_dual_auth_headers(),
)
if resp.status_code != 200:
return ""
databases = resp.json()
if not databases:
return "No databases found."
@@ -287,8 +286,8 @@ async def save_conversation(conv_id: str, user_text: str, user_id: str = "admin"
if assistant_text:
messages.append({"id": str(uuid.uuid4()), "conversation_id": conv_id, "role": "assistant", "text": assistant_text.strip(), "state": None, "created_at": datetime.utcnow().isoformat()})
payload = {"conversation_id": conv_id, "title": clean_title(user_text)[:TITLE_MAX_LENGTH], "user_id": user_id, "messages": messages}
async with httpx.AsyncClient(timeout=10) as client:
await client.post(SAVE_API_URL, json=payload, headers=headers)
client = get_shared_http_client(timeout=10)
await client.post(SAVE_API_URL, json=payload, headers=headers)
logger.reflect("Conversation saved", payload={"conv_id": conv_id, "user_id": user_id, "messages": len(messages)}, extra={"src": "AgentChat.Persistence"})
except Exception as e:
logger.explore("Save conversation failed", payload={"conv_id": conv_id}, error=str(e), extra={"src": "AgentChat.Persistence"})

View File

@@ -10,7 +10,6 @@
import inspect as _inspect
import os
import httpx
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
@@ -23,6 +22,7 @@ import pydantic_core as _pydantic_core
from ss_tools.agent._config import AGENT_CONFIRM_TOOLS, AGENT_INTERRUPT_BEFORE as _INTERRUPT_BEFORE, FASTAPI_URL
from ss_tools.agent._llm_params import chat_openai_kwargs
from ss_tools.shared._llm_http import get_shared_http_client
from ss_tools.shared.logger import logger
_original_transform = _openai_transform._async_transform_recursive
@@ -90,13 +90,13 @@ async def _fetch_llm_config() -> dict | None:
global _llm_config
try:
fastapi_url = FASTAPI_URL
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(f"{fastapi_url}/api/agent/llm-config")
if resp.status_code == 200:
config = resp.json()
if config.get("configured"):
_llm_config = config
return config
client = get_shared_http_client(timeout=10)
resp = await client.get(f"{fastapi_url}/api/agent/llm-config")
if resp.status_code == 200:
config = resp.json()
if config.get("configured"):
_llm_config = config
return config
except Exception as e:
logger.explore("Failed to fetch LLM config from FastAPI", error=str(e), extra={"src": "AgentChat.LangGraph.Setup"})
return _llm_config
@@ -133,7 +133,10 @@ async def create_agent(tools: list, env_id: str | None = None, interrupt_before:
else:
raise RuntimeError("No LLM provider configured in backend. Configure one via Settings → AI Providers in the web UI.")
logger.reason("Creating LangGraph agent", payload={"model": model, "tools_count": len(tools), "env_id": env_id}, extra={"src": "AgentChat.LangGraph.Setup"})
llm = ChatOpenAI(**chat_openai_kwargs(model=model, base_url=base_url, api_key=api_key, max_tokens=2048))
llm = ChatOpenAI(
http_client=get_shared_http_client(),
**chat_openai_kwargs(model=model, base_url=base_url, api_key=api_key, max_tokens=2048),
)
prompt = (
"You are a Superset Tools assistant. You have access to tools for searching "
"dashboards, managing maintenance, running migrations and backups, "

View File

@@ -22,6 +22,7 @@ from ss_tools.agent._config import (
)
from ss_tools.shared.cot_logger import seed_trace_id
from ss_tools.shared.logger import logger
from ss_tools.shared.ssl import httpx_verify
def _find_free_port(start_port: int, max_attempts: int = 100) -> int:
@@ -46,9 +47,15 @@ def _fetch_llm_config() -> dict | None:
service_token = SERVICE_JWT
headers = {"Authorization": f"Bearer {service_token}"} if service_token else {}
ssl_ctx = httpx_verify()
for attempt in range(6):
try:
resp = httpx.get(f"{FASTAPI_URL}/api/agent/llm-config", headers=headers, timeout=5)
resp = httpx.get(
f"{FASTAPI_URL}/api/agent/llm-config",
headers=headers,
timeout=5,
verify=ssl_ctx,
)
resp.raise_for_status()
config = resp.json()
if config.get("configured"):

View File

@@ -19,6 +19,7 @@ from pydantic import BaseModel, Field
from ss_tools.agent._config import FASTAPI_URL, SERVICE_JWT as _SERVICE_JWT
from ss_tools.agent.context import get_service_jwt, get_user_jwt, get_user_role
from ss_tools.shared._llm_http import get_shared_http_client
from ss_tools.shared.logger import logger
TOOL_RESPONSE_LIMIT = 4000
@@ -249,19 +250,19 @@ async def _execute_with_timeout(tool_name: str, tool_fn, is_write: bool = False,
# @BRIEF Async HTTP GET to FastAPI with dual-auth headers.
async def _get(path: str, params: dict[str, Any] | None = None) -> httpx.Response:
async def _request() -> httpx.Response:
async with httpx.AsyncClient(timeout=TOOL_TIMEOUT_SECONDS) as client:
resp = await client.get(
f"{FASTAPI_URL}{path}",
params=params,
headers=_dual_auth_headers(),
client = get_shared_http_client(timeout=TOOL_TIMEOUT_SECONDS)
resp = await client.get(
f"{FASTAPI_URL}{path}",
params=params,
headers=_dual_auth_headers(),
)
if resp.status_code in {502, 503, 504}:
raise httpx.HTTPStatusError(
f"Transient FastAPI error {resp.status_code}",
request=resp.request,
response=resp,
)
if resp.status_code in {502, 503, 504}:
raise httpx.HTTPStatusError(
f"Transient FastAPI error {resp.status_code}",
request=resp.request,
response=resp,
)
return resp
return resp
try:
return await _execute_with_timeout(path, lambda: _retry_read_tool(path, _request), timeout_s=TOOL_TIMEOUT_SECONDS)
@@ -279,13 +280,13 @@ async def _post(
params: dict[str, Any] | None = None,
) -> httpx.Response:
async def _request() -> httpx.Response:
async with httpx.AsyncClient(timeout=TOOL_TIMEOUT_SECONDS) as client:
return await client.post(
f"{FASTAPI_URL}{path}",
json=payload or {},
params=params,
headers=_dual_auth_headers(),
)
client = get_shared_http_client(timeout=TOOL_TIMEOUT_SECONDS)
return await client.post(
f"{FASTAPI_URL}{path}",
json=payload or {},
params=params,
headers=_dual_auth_headers(),
)
try:
return await _execute_with_timeout(path, _request, is_write=True, timeout_s=TOOL_TIMEOUT_SECONDS)