## Fixes - [#3] Status events now add/update a status ActivityNode in timeline (upsertWsStatusNode) - [#4] Done callback adds done node; error callback adds error node + sets session.status="error" - [#5] handleRegenerate fully synced with workspace: status/tool/card/done/error all handled - [#1][#2] localStorage persistence: completed/error sessions auto-saved, lazy-loaded on demand - [#1] handleSelectConversation preloads workspace sessions from localStorage for history messages - Refactored completeWsSession to include done ActivityNode in timeline - Added errorWsSession, upsertWsStatusNode, saveWsToStorage, loadWsFromStorage helpers ## Known limitation - [#7] workspace_card merge:true not yet used by backend (all cards are append-only for now) - History workspace recovery depends on localStorage (browser-local, not cross-device) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
"""Knowledge base search tool — calls KB Agent (Azure AI Search).
|
|
|
|
Includes a single retry on ReadTimeout to handle Azure App Service cold
|
|
starts (can take 20-30s). All exceptions are caught so the tool returns
|
|
a user-friendly message instead of crashing the ReAct agent loop.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
import httpx
|
|
from langchain_core.tools import tool
|
|
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@tool
|
|
async def kb_search(query: str) -> str:
|
|
"""Search the internal knowledge base for documents related to a query.
|
|
|
|
Use this tool when the user asks about internal products, technical
|
|
documentation, project plans, or anything that might be covered by
|
|
the company knowledge base.
|
|
|
|
Args:
|
|
query: The search query in natural language.
|
|
"""
|
|
url = f"{settings.kb_agent_url}{settings.kb_agent_search_path}"
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"api-key": settings.kb_agent_api_key,
|
|
}
|
|
payload = {
|
|
"query": query,
|
|
"top": 5,
|
|
"search_mode": "hybrid",
|
|
}
|
|
|
|
for attempt in range(2):
|
|
try:
|
|
async with httpx.AsyncClient(timeout=settings.kb_agent_search_timeout_sec) as client:
|
|
resp = await client.post(url, json=payload, headers=headers)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
results = data.get("results", [])
|
|
if not results:
|
|
return "知识库中未找到相关内容。"
|
|
|
|
parts: list[str] = []
|
|
for r in results:
|
|
title = r.get("title", "Untitled")
|
|
content = r.get("content", "")
|
|
category = r.get("category", "")
|
|
# Truncate very long content to keep context manageable
|
|
if len(content) > 1500:
|
|
content = content[:1500] + "..."
|
|
header = f"[{title}]"
|
|
if category:
|
|
header += f" ({category})"
|
|
parts.append(f"{header}\n{content}")
|
|
|
|
return "\n\n---\n\n".join(parts)
|
|
|
|
except httpx.ReadTimeout:
|
|
if attempt == 0:
|
|
logger.warning("KB Agent read timeout (attempt 1), retrying after 2s...")
|
|
await asyncio.sleep(2)
|
|
continue
|
|
logger.error("KB Agent read timeout after retry")
|
|
return "知识库检索超时,请稍后重试。"
|
|
|
|
except Exception as exc:
|
|
logger.error("KB Agent search failed: %s", exc, exc_info=True)
|
|
return f"知识库检索出错:{exc}"
|