refactor(agent): use local JWT decoder instead of src.core.auth.jwt
Replace src.core.auth.jwt import with lightweight _jwt_decoder.py that uses jose.jwt directly with JWT_SECRET env var. Avoids pulling AuthConfig → AUTH_DATABASE_URL/SECRET_KEY validators → SQLAlchemy models into agent. Removed: AUTH_SECRET_KEY, AUTH_DATABASE_URL from agent compose env. Removed: src/core/auth/ copies from Dockerfile.agent COPY section. Added: src/agent/_jwt_decoder.py — stateless JWT validation.
This commit is contained in:
@@ -24,6 +24,7 @@ TITLE_MAX_LENGTH = 80
|
||||
|
||||
# ── Rule-based title cleaning ────────────────────────────────────
|
||||
|
||||
|
||||
# #region AgentChat.Persistence.CleanTitle [C:2] [TYPE Function] [SEMANTICS agent-chat,persistence,title]
|
||||
# @ingroup AgentChat
|
||||
# @BRIEF Clean raw user text into a readable conversation title — strip file markers,
|
||||
@@ -47,11 +48,11 @@ def clean_title(user_text: str) -> str: # noqa: C901
|
||||
# ── Strip file upload markers (cut before first occurrence) ──
|
||||
file_markers = [
|
||||
"\n--- Uploaded file content ---",
|
||||
"--- Uploaded file content ---", # no leading newline
|
||||
"--- Uploaded file content ---", # no leading newline
|
||||
"\n[PRE-FETCHED DATA",
|
||||
"[PRE-FETCHED DATA", # no leading newline
|
||||
"[PRE-FETCHED DATA", # no leading newline
|
||||
"\n[/PRE-FETCHED DATA]",
|
||||
"[/PRE-FETCHED DATA]", # no leading newline
|
||||
"[/PRE-FETCHED DATA]", # no leading newline
|
||||
]
|
||||
cut_pos = len(text)
|
||||
for marker in file_markers:
|
||||
@@ -67,11 +68,11 @@ def clean_title(user_text: str) -> str: # noqa: C901
|
||||
# ── Take first meaningful segment ──
|
||||
# Priority: first sentence ending with .!?, else first line, else full text
|
||||
sentence_end = -1
|
||||
for m in re.finditer(r'[.!?]\s', text):
|
||||
for m in re.finditer(r"[.!?]\s", text):
|
||||
sentence_end = m.start()
|
||||
break
|
||||
if sentence_end > 3: # don't cut "Привет." into "Привет"
|
||||
text = text[:sentence_end + 1].strip()
|
||||
text = text[: sentence_end + 1].strip()
|
||||
elif "\n" in text:
|
||||
text = text.split("\n")[0].strip()
|
||||
|
||||
@@ -86,6 +87,7 @@ def clean_title(user_text: str) -> str: # noqa: C901
|
||||
if text.startswith("http://") or text.startswith("https://"):
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
domain = urlparse(text).netloc or "ссылка"
|
||||
except Exception:
|
||||
domain = "ссылка"
|
||||
@@ -108,6 +110,8 @@ def clean_title(user_text: str) -> str: # noqa: C901
|
||||
return "Новый диалог"
|
||||
|
||||
return text
|
||||
|
||||
|
||||
# #endregion AgentChat.Persistence.CleanTitle
|
||||
|
||||
|
||||
@@ -124,6 +128,8 @@ def detect_message_state(text: str) -> str | None:
|
||||
if any(m in t for m in error_markers):
|
||||
return "error"
|
||||
return None
|
||||
|
||||
|
||||
# #endregion AgentChat.Persistence.DetectState
|
||||
|
||||
|
||||
@@ -133,11 +139,14 @@ def detect_message_state(text: str) -> str | None:
|
||||
# @POST Returns user_id string or "unknown".
|
||||
def extract_user_id(jwt_str: str) -> str:
|
||||
try:
|
||||
from src.core.auth.jwt import decode_token
|
||||
from src.agent._jwt_decoder import decode_token
|
||||
|
||||
payload = decode_token(jwt_str)
|
||||
return payload.get("sub", payload.get("user_id", "unknown"))
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
# #endregion AgentChat.Persistence.ExtractUserId
|
||||
|
||||
|
||||
@@ -182,11 +191,7 @@ async def _call_llm_for_title(user_text: str) -> str | None:
|
||||
if not clean_text or clean_text in ("Новый диалог",):
|
||||
return None
|
||||
|
||||
prompt = (
|
||||
f"Сгенерируй заголовок из 3-5 слов на русском для диалога. "
|
||||
f"Только заголовок, без кавычек и пояснений.\n\n"
|
||||
f"Диалог: {clean_text}"
|
||||
)
|
||||
prompt = f"Сгенерируй заголовок из 3-5 слов на русском для диалога. Только заголовок, без кавычек и пояснений.\n\nДиалог: {clean_text}"
|
||||
|
||||
api_key = config.get("api_key", "")
|
||||
base_url = config.get("base_url", "")
|
||||
@@ -214,7 +219,8 @@ async def _call_llm_for_title(user_text: str) -> str | None:
|
||||
if resp.status_code != 200:
|
||||
_logger.explore(
|
||||
"LLM title: API error",
|
||||
payload={"status": resp.status_code, "reason": resp.reason_phrase}, error=f"HTTP {resp.status_code}: {resp.text[:100]}",
|
||||
payload={"status": resp.status_code, "reason": resp.reason_phrase},
|
||||
error=f"HTTP {resp.status_code}: {resp.text[:100]}",
|
||||
extra={"src": "AgentChat.Persistence"},
|
||||
)
|
||||
return None
|
||||
@@ -222,7 +228,7 @@ async def _call_llm_for_title(user_text: str) -> str | None:
|
||||
title = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
if title:
|
||||
# Strip markdown/formatting
|
||||
title = re.sub(r'[*_`#"\']', '', title).strip()
|
||||
title = re.sub(r'[*_`#"\']', "", title).strip()
|
||||
title = title[:100] # safety cap
|
||||
if title:
|
||||
return title
|
||||
@@ -274,11 +280,14 @@ async def generate_llm_title(conv_id: str, user_text: str) -> None:
|
||||
except Exception as e:
|
||||
logger.explore(
|
||||
"LLM title save failed",
|
||||
payload={"conv_id": conv_id}, error=str(e),
|
||||
payload={"conv_id": conv_id},
|
||||
error=str(e),
|
||||
extra={"src": "AgentChat.Persistence"},
|
||||
)
|
||||
finally:
|
||||
_title_locks.pop(conv_id, None)
|
||||
|
||||
|
||||
# #endregion AgentChat.Persistence.GenerateLlmTitle
|
||||
|
||||
|
||||
@@ -286,6 +295,7 @@ async def generate_llm_title(conv_id: str, user_text: str) -> None:
|
||||
# Prefetch dashboards
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
# #region AgentChat.Persistence.PrefetchDashboards [C:3] [TYPE Function] [SEMANTICS agent-chat,persistence,prefetch]
|
||||
# @ingroup AgentChat
|
||||
# @BRIEF Pre-fetch dashboard data so the LLM has it in context without needing to call a tool.
|
||||
@@ -296,6 +306,7 @@ async def generate_llm_title(conv_id: str, user_text: str) -> None:
|
||||
async def prefetch_dashboards(env_id: str) -> str:
|
||||
try:
|
||||
from src.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",
|
||||
@@ -326,10 +337,13 @@ async def prefetch_dashboards(env_id: str) -> str:
|
||||
except Exception as e:
|
||||
logger.explore(
|
||||
"Prefetch dashboards failed",
|
||||
payload={"env_id": env_id}, error=str(e),
|
||||
payload={"env_id": env_id},
|
||||
error=str(e),
|
||||
extra={"src": "AgentChat.Persistence.PrefetchDashboards"},
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
# #endregion AgentChat.Persistence.PrefetchDashboards
|
||||
|
||||
|
||||
@@ -341,6 +355,7 @@ async def prefetch_dashboards(env_id: str) -> str:
|
||||
async def prefetch_databases(env_id: str) -> str:
|
||||
try:
|
||||
from src.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",
|
||||
@@ -365,10 +380,13 @@ async def prefetch_databases(env_id: str) -> str:
|
||||
except Exception as e:
|
||||
logger.explore(
|
||||
"Prefetch databases failed",
|
||||
payload={"env_id": env_id}, error=str(e),
|
||||
payload={"env_id": env_id},
|
||||
error=str(e),
|
||||
extra={"src": "AgentChat.Persistence.PrefetchDatabases"},
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
# #endregion AgentChat.Persistence.PrefetchDatabases
|
||||
|
||||
|
||||
@@ -404,14 +422,16 @@ async def save_conversation(conv_id: str, user_text: str, user_id: str = "admin"
|
||||
]
|
||||
if assistant_text.strip():
|
||||
assistant_state = detect_message_state(assistant_text)
|
||||
messages.append({
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation_id": conv_id,
|
||||
"role": "assistant",
|
||||
"text": assistant_text.strip(),
|
||||
"state": assistant_state,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
})
|
||||
messages.append(
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation_id": conv_id,
|
||||
"role": "assistant",
|
||||
"text": assistant_text.strip(),
|
||||
"state": assistant_state,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
title = clean_title(user_text)
|
||||
payload = {
|
||||
@@ -431,8 +451,11 @@ async def save_conversation(conv_id: str, user_text: str, user_id: str = "admin"
|
||||
except Exception as e:
|
||||
logger.explore(
|
||||
"Failed to save conversation",
|
||||
payload={"conv_id": conv_id}, error=str(e),
|
||||
payload={"conv_id": conv_id},
|
||||
error=str(e),
|
||||
extra={"src": "AgentChat.Persistence"},
|
||||
)
|
||||
|
||||
|
||||
# #endregion AgentChat.Persistence.SaveConversation
|
||||
# #endregion AgentChat.Persistence
|
||||
|
||||
Reference in New Issue
Block a user