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:
@@ -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. "
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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, "
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user