feat: knowledge base first OK

This commit is contained in:
qixinbo
2026-03-29 00:20:53 +08:00
parent bd7776d1b7
commit 92e8c40826
17 changed files with 3357 additions and 10 deletions
+257
View File
@@ -0,0 +1,257 @@
from typing import List, Optional
import io
import json
from fastapi import APIRouter, HTTPException
from fastapi import UploadFile, File, Form
from openai import OpenAI
import pandas as pd
from app.schemas.knowledge import (
KnowledgeBase,
KnowledgeBaseCreate,
KnowledgeConnectionTestRequest,
KnowledgeConnectionTestResponse,
KnowledgeGlobalConfig,
KnowledgeGlobalConfigUpdate,
KnowledgeBaseUpdate,
KnowledgeDocument,
KnowledgeDocumentCreate,
KnowledgeDocumentUpdate,
KnowledgeSearchRequest,
KnowledgeSearchResponse,
)
from app.services.knowledge_base_store import knowledge_base_store
from app.services.knowledge_global_config_store import knowledge_global_config_store
from app.services.knowledge_index import knowledge_index_service
from app.services.openai_compat import normalize_openai_base_url
router = APIRouter()
def _mask_api_key(value: Optional[str]) -> Optional[str]:
if not value:
return None
if len(value) <= 8:
return "*" * len(value)
return f"{value[:4]}{'*' * (len(value) - 8)}{value[-4:]}"
def _extract_upload_text(filename: str, content: bytes) -> str:
lower = filename.lower()
if lower.endswith((".txt", ".md", ".markdown", ".json", ".yaml", ".yml", ".log", ".xml", ".html", ".htm")):
try:
return content.decode("utf-8")
except UnicodeDecodeError:
return content.decode("utf-8", errors="ignore")
if lower.endswith(".csv"):
df = pd.read_csv(io.BytesIO(content))
return df.to_csv(index=False)
if lower.endswith((".xls", ".xlsx")):
df = pd.read_excel(io.BytesIO(content))
return df.to_csv(index=False)
raise ValueError("Unsupported file type")
@router.get("/knowledge-bases/global-config", response_model=KnowledgeGlobalConfig)
def get_knowledge_global_config():
config = knowledge_global_config_store.get()
raw_api_key = config.get("api_key")
return {
"api_base": config.get("api_base"),
"api_key": None,
"api_key_masked": _mask_api_key(raw_api_key),
"has_api_key": bool(raw_api_key),
"default_embedding_model": config.get("default_embedding_model"),
}
@router.put("/knowledge-bases/global-config", response_model=KnowledgeGlobalConfig)
def update_knowledge_global_config(payload: KnowledgeGlobalConfigUpdate):
updated = knowledge_global_config_store.update(payload.model_dump(exclude_unset=True))
raw_api_key = updated.get("api_key")
return {
"api_base": updated.get("api_base"),
"api_key": None,
"api_key_masked": _mask_api_key(raw_api_key),
"has_api_key": bool(raw_api_key),
"default_embedding_model": updated.get("default_embedding_model"),
}
@router.post("/knowledge-bases/global-config/test-connection", response_model=KnowledgeConnectionTestResponse)
def test_knowledge_global_connection(payload: KnowledgeConnectionTestRequest):
saved = knowledge_global_config_store.get()
api_base = normalize_openai_base_url(payload.api_base or saved.get("api_base") or "")
api_key = payload.api_key or saved.get("api_key")
model_name = (payload.model_name or "").strip()
if not api_base:
raise HTTPException(status_code=400, detail="API Base 未配置")
if not api_key:
raise HTTPException(status_code=400, detail="API Key 未配置")
if not model_name:
raise HTTPException(status_code=400, detail="测试连接必须显式填写向量模型名称")
if not api_base:
raise HTTPException(status_code=400, detail="API Base 未配置")
try:
client = OpenAI(
api_key=api_key,
base_url=api_base,
)
embedding_resp = client.embeddings.create(
model=model_name,
input="connection test",
)
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Embedding调用失败: {exc}")
dimension = None
if getattr(embedding_resp, "data", None):
first = embedding_resp.data[0]
vector = getattr(first, "embedding", None)
if isinstance(vector, list):
dimension = len(vector)
return {
"success": True,
"message": "连接成功,Embedding调用正常",
"model_name": model_name,
"embedding_dimension": dimension,
"resolved_api_base": api_base,
"available_models": [],
}
@router.get("/knowledge-bases", response_model=List[KnowledgeBase])
def list_knowledge_bases(project_id: Optional[int] = None):
return knowledge_base_store.list(project_id=project_id)
@router.post("/knowledge-bases", response_model=KnowledgeBase)
def create_knowledge_base(payload: KnowledgeBaseCreate):
return knowledge_base_store.create(payload.model_dump())
@router.get("/knowledge-bases/{kb_id}", response_model=KnowledgeBase)
def get_knowledge_base(kb_id: str):
kb = knowledge_base_store.get(kb_id)
if not kb:
raise HTTPException(status_code=404, detail="Knowledge base not found")
return kb
@router.put("/knowledge-bases/{kb_id}", response_model=KnowledgeBase)
def update_knowledge_base(kb_id: str, payload: KnowledgeBaseUpdate):
kb = knowledge_base_store.update(kb_id, payload.model_dump(exclude_unset=True))
if not kb:
raise HTTPException(status_code=404, detail="Knowledge base not found")
return kb
@router.delete("/knowledge-bases/{kb_id}")
def delete_knowledge_base(kb_id: str):
deleted = knowledge_base_store.delete(kb_id)
if not deleted:
raise HTTPException(status_code=404, detail="Knowledge base not found")
return {"status": "success"}
@router.get("/knowledge-bases/{kb_id}/documents", response_model=List[KnowledgeDocument])
def list_knowledge_documents(kb_id: str):
kb = knowledge_base_store.get(kb_id)
if not kb:
raise HTTPException(status_code=404, detail="Knowledge base not found")
return kb.get("documents", [])
@router.post("/knowledge-bases/{kb_id}/documents", response_model=KnowledgeDocument)
def create_knowledge_document(kb_id: str, payload: KnowledgeDocumentCreate):
doc = knowledge_base_store.create_document(kb_id=kb_id, payload=payload.model_dump())
if not doc:
raise HTTPException(status_code=404, detail="Knowledge base not found")
return doc
@router.put("/knowledge-bases/{kb_id}/documents/{doc_id}", response_model=KnowledgeDocument)
def update_knowledge_document(kb_id: str, doc_id: str, payload: KnowledgeDocumentUpdate):
doc = knowledge_base_store.update_document(
kb_id=kb_id,
doc_id=doc_id,
payload=payload.model_dump(exclude_unset=True),
)
if not doc:
raise HTTPException(status_code=404, detail="Knowledge document not found")
return doc
@router.delete("/knowledge-bases/{kb_id}/documents/{doc_id}")
def delete_knowledge_document(kb_id: str, doc_id: str):
deleted = knowledge_base_store.delete_document(kb_id=kb_id, doc_id=doc_id)
if not deleted:
raise HTTPException(status_code=404, detail="Knowledge document not found")
return {"status": "success"}
@router.post("/knowledge-bases/{kb_id}/reindex")
def reindex_knowledge_base(kb_id: str):
try:
return knowledge_index_service.reindex(kb_id)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc))
@router.post("/knowledge-bases/{kb_id}/search", response_model=KnowledgeSearchResponse)
def search_knowledge_base(kb_id: str, payload: KnowledgeSearchRequest):
try:
result = knowledge_index_service.search(
kb_id=kb_id,
query=payload.query,
top_k=payload.top_k,
)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc))
return result
@router.post("/knowledge-bases/{kb_id}/documents/upload")
async def upload_knowledge_documents(
kb_id: str,
files: List[UploadFile] = File(...),
metadata: Optional[str] = Form(default=None),
):
kb = knowledge_base_store.get(kb_id)
if not kb:
raise HTTPException(status_code=404, detail="Knowledge base not found")
metadata_payload: dict[str, Any] = {}
if metadata:
try:
parsed_metadata = json.loads(metadata)
if isinstance(parsed_metadata, dict):
metadata_payload = parsed_metadata
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="metadata 必须是合法 JSON 对象")
created: List[dict[str, Any]] = []
for file in files:
filename = file.filename or "untitled"
content = await file.read()
if not content:
continue
if len(content) > 5 * 1024 * 1024:
raise HTTPException(status_code=400, detail=f"文件过大: {filename}")
try:
text = _extract_upload_text(filename, content)
except Exception:
raise HTTPException(status_code=400, detail=f"不支持的文件类型: {filename}")
doc = knowledge_base_store.create_document(
kb_id=kb_id,
payload={
"title": filename,
"content": text,
"metadata": {**metadata_payload, "source": "upload", "filename": filename},
},
)
if doc:
created.append(doc)
return {"status": "success", "count": len(created), "documents": created}
+2
View File
@@ -19,3 +19,5 @@ current_data_source: ContextVar[str] = ContextVar("current_data_source", default
# Any file URL attached to the request
current_file_url: ContextVar[Optional[str]] = ContextVar("current_file_url", default=None)
current_knowledge_base_id: ContextVar[Optional[str]] = ContextVar("current_knowledge_base_id", default=None)
+2
View File
@@ -204,10 +204,12 @@ class NanobotIntegration:
from app.tools.nl2sql import NL2SQLTool
from app.tools.visualization import VisualizationTool
from app.tools.get_schema import GetDatabaseSchemaTool
from app.tools.knowledge_base import KnowledgeBaseRetrieveTool
from app.tools.subagent import ListSubagentsTool, InvokeSubagentTool
agent.tools.register(NL2SQLTool())
agent.tools.register(VisualizationTool())
agent.tools.register(GetDatabaseSchemaTool())
agent.tools.register(KnowledgeBaseRetrieveTool())
agent.tools.register(ListSubagentsTool(project_id=project_id))
agent.tools.register(InvokeSubagentTool(project_id=project_id))
+162
View File
@@ -0,0 +1,162 @@
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field, field_validator
class KnowledgeDocumentBase(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
content: str = Field(..., min_length=1)
metadata: Dict[str, Any] = Field(default_factory=dict)
class KnowledgeDocumentCreate(KnowledgeDocumentBase):
pass
class KnowledgeDocumentUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=200)
content: Optional[str] = Field(None, min_length=1)
metadata: Optional[Dict[str, Any]] = None
class KnowledgeDocument(KnowledgeDocumentBase):
id: str
created_at: datetime
updated_at: datetime
class KnowledgeBaseConfigBase(BaseModel):
name: str = Field(..., min_length=1, max_length=120)
description: Optional[str] = None
project_id: Optional[int] = None
embedding_model: Optional[str] = None
chunk_size: int = Field(default=512, ge=64, le=4096)
chunk_overlap: int = Field(default=50, ge=0, le=512)
top_k: int = Field(default=3, ge=1, le=20)
is_active: bool = True
class KnowledgeBaseCreate(KnowledgeBaseConfigBase):
pass
class KnowledgeBaseUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=120)
description: Optional[str] = None
project_id: Optional[int] = None
embedding_model: Optional[str] = None
chunk_size: Optional[int] = Field(None, ge=64, le=4096)
chunk_overlap: Optional[int] = Field(None, ge=0, le=512)
top_k: Optional[int] = Field(None, ge=1, le=20)
is_active: Optional[bool] = None
class KnowledgeBase(KnowledgeBaseConfigBase):
id: str
created_at: datetime
updated_at: datetime
documents: List[KnowledgeDocument] = Field(default_factory=list)
class KnowledgeSearchRequest(BaseModel):
query: str = Field(..., min_length=1)
top_k: Optional[int] = Field(default=None, ge=1, le=20)
class KnowledgeSearchHit(BaseModel):
doc_id: str
title: str
chunk: str
score: float
metadata: Dict[str, Any] = Field(default_factory=dict)
class KnowledgeSearchResponse(BaseModel):
answer: str
hits: List[KnowledgeSearchHit] = Field(default_factory=list)
class KnowledgeGlobalConfigUpdate(BaseModel):
api_base: Optional[str] = None
api_key: Optional[str] = None
default_embedding_model: Optional[str] = None
@field_validator("api_base")
@classmethod
def validate_api_base(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
normalized = value.strip()
if not normalized:
return None
if not (normalized.startswith("http://") or normalized.startswith("https://")):
raise ValueError("api_base must start with http:// or https://")
return normalized.rstrip("/")
@field_validator("api_key")
@classmethod
def validate_api_key(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
normalized = value.strip()
if not normalized:
return None
if len(normalized) > 512:
raise ValueError("api_key is too long")
return normalized
@field_validator("default_embedding_model")
@classmethod
def validate_default_embedding_model(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
normalized = value.strip()
if not normalized:
return None
if len(normalized) > 200:
raise ValueError("default_embedding_model is too long")
return normalized
class KnowledgeGlobalConfig(BaseModel):
api_base: Optional[str] = None
api_key: Optional[str] = None
api_key_masked: Optional[str] = None
has_api_key: bool = False
default_embedding_model: Optional[str] = None
class KnowledgeConnectionTestRequest(BaseModel):
api_base: Optional[str] = None
api_key: Optional[str] = None
model_name: Optional[str] = None
@field_validator("api_base")
@classmethod
def validate_test_api_base(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
normalized = value.strip()
if not normalized:
return None
if not (normalized.startswith("http://") or normalized.startswith("https://")):
raise ValueError("api_base must start with http:// or https://")
return normalized.rstrip("/")
@field_validator("api_key", "model_name")
@classmethod
def normalize_test_value(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
normalized = value.strip()
return normalized or None
class KnowledgeConnectionTestResponse(BaseModel):
success: bool
message: str
model_name: Optional[str] = None
embedding_dimension: Optional[int] = None
resolved_api_base: Optional[str] = None
available_models: List[str] = Field(default_factory=list)
@@ -0,0 +1,188 @@
import json
import threading
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from app.core.data_root import get_data_root
def _utcnow_iso() -> str:
return datetime.now(timezone.utc).isoformat()
class KnowledgeBaseStore:
def __init__(self) -> None:
self._lock = threading.RLock()
@staticmethod
def _file_path() -> Path:
return get_data_root() / "knowledge_bases.json"
def _read(self) -> List[Dict[str, Any]]:
file_path = self._file_path()
if not file_path.exists():
return []
try:
with file_path.open("r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError):
return []
if not isinstance(data, list):
return []
return data
def _write(self, data: List[Dict[str, Any]]) -> None:
file_path = self._file_path()
file_path.parent.mkdir(parents=True, exist_ok=True)
with file_path.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
@staticmethod
def _normalize_documents(item: Dict[str, Any]) -> None:
docs = item.get("documents")
if not isinstance(docs, list):
item["documents"] = []
return
normalized: List[Dict[str, Any]] = []
for doc in docs:
if not isinstance(doc, dict):
continue
if not doc.get("id"):
doc["id"] = str(uuid.uuid4())
now = _utcnow_iso()
doc.setdefault("created_at", now)
doc.setdefault("updated_at", now)
doc.setdefault("metadata", {})
normalized.append(doc)
item["documents"] = normalized
def list(self, project_id: Optional[int] = None) -> List[Dict[str, Any]]:
with self._lock:
data = self._read()
for item in data:
self._normalize_documents(item)
if project_id is None:
return data
return [item for item in data if item.get("project_id") == project_id]
def get(self, kb_id: str) -> Optional[Dict[str, Any]]:
with self._lock:
for item in self._read():
if item.get("id") == kb_id:
self._normalize_documents(item)
return item
return None
def create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
with self._lock:
data = self._read()
now = _utcnow_iso()
item = {
"id": str(uuid.uuid4()),
"name": payload["name"],
"description": payload.get("description"),
"project_id": payload.get("project_id"),
"embedding_model": payload.get("embedding_model"),
"chunk_size": payload.get("chunk_size", 512),
"chunk_overlap": payload.get("chunk_overlap", 50),
"top_k": payload.get("top_k", 3),
"is_active": payload.get("is_active", True),
"created_at": now,
"updated_at": now,
"documents": [],
}
data.append(item)
self._write(data)
return item
def update(self, kb_id: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
with self._lock:
data = self._read()
for idx, item in enumerate(data):
if item.get("id") != kb_id:
continue
for key, value in payload.items():
item[key] = value
item["updated_at"] = _utcnow_iso()
self._normalize_documents(item)
data[idx] = item
self._write(data)
return item
return None
def delete(self, kb_id: str) -> bool:
with self._lock:
data = self._read()
filtered = [item for item in data if item.get("id") != kb_id]
if len(filtered) == len(data):
return False
self._write(filtered)
return True
def create_document(self, kb_id: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
with self._lock:
data = self._read()
for idx, item in enumerate(data):
if item.get("id") != kb_id:
continue
now = _utcnow_iso()
doc = {
"id": str(uuid.uuid4()),
"title": payload["title"],
"content": payload["content"],
"metadata": payload.get("metadata", {}),
"created_at": now,
"updated_at": now,
}
self._normalize_documents(item)
item["documents"].append(doc)
item["updated_at"] = now
data[idx] = item
self._write(data)
return doc
return None
def update_document(self, kb_id: str, doc_id: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
with self._lock:
data = self._read()
for kb_idx, item in enumerate(data):
if item.get("id") != kb_id:
continue
self._normalize_documents(item)
docs = item["documents"]
for doc_idx, doc in enumerate(docs):
if doc.get("id") != doc_id:
continue
for key, value in payload.items():
doc[key] = value
doc["updated_at"] = _utcnow_iso()
docs[doc_idx] = doc
item["updated_at"] = _utcnow_iso()
data[kb_idx] = item
self._write(data)
return doc
return None
return None
def delete_document(self, kb_id: str, doc_id: str) -> bool:
with self._lock:
data = self._read()
for kb_idx, item in enumerate(data):
if item.get("id") != kb_id:
continue
self._normalize_documents(item)
docs = item["documents"]
filtered = [doc for doc in docs if doc.get("id") != doc_id]
if len(filtered) == len(docs):
return False
item["documents"] = filtered
item["updated_at"] = _utcnow_iso()
data[kb_idx] = item
self._write(data)
return True
return False
knowledge_base_store = KnowledgeBaseStore()
@@ -0,0 +1,58 @@
import json
import threading
from pathlib import Path
from typing import Any, Dict
from app.core.data_root import get_data_root
class KnowledgeGlobalConfigStore:
def __init__(self) -> None:
self._lock = threading.RLock()
@staticmethod
def _file_path() -> Path:
return get_data_root() / "knowledge_global_config.json"
def _read(self) -> Dict[str, Any]:
file_path = self._file_path()
if not file_path.exists():
return {}
try:
with file_path.open("r", encoding="utf-8") as file_obj:
data = json.load(file_obj)
except (OSError, json.JSONDecodeError):
return {}
if not isinstance(data, dict):
return {}
return data
def _write(self, data: Dict[str, Any]) -> None:
file_path = self._file_path()
file_path.parent.mkdir(parents=True, exist_ok=True)
with file_path.open("w", encoding="utf-8") as file_obj:
json.dump(data, file_obj, indent=2, ensure_ascii=False)
def get(self) -> Dict[str, Any]:
with self._lock:
data = self._read()
return {
"api_base": data.get("api_base"),
"api_key": data.get("api_key"),
"default_embedding_model": data.get("default_embedding_model"),
}
def update(self, payload: Dict[str, Any]) -> Dict[str, Any]:
with self._lock:
current = self.get()
if "api_base" in payload:
current["api_base"] = payload.get("api_base")
if "api_key" in payload:
current["api_key"] = payload.get("api_key")
if "default_embedding_model" in payload:
current["default_embedding_model"] = payload.get("default_embedding_model")
self._write(current)
return current
knowledge_global_config_store = KnowledgeGlobalConfigStore()
+250
View File
@@ -0,0 +1,250 @@
import math
import re
import threading
from dataclasses import dataclass
from typing import Any, Dict, List, Tuple
from app.services.knowledge_base_store import knowledge_base_store
from app.services.knowledge_global_config_store import knowledge_global_config_store
from app.services.openai_compat import normalize_openai_base_url
try:
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
LLAMAINDEX_AVAILABLE = True
except Exception:
Document = Any
VectorStoreIndex = Any
SentenceSplitter = Any
LLAMAINDEX_AVAILABLE = False
def _tokenize(text: str) -> List[str]:
return re.findall(r"[a-zA-Z0-9]+|[\u4e00-\u9fff]", (text or "").lower())
def _normalize_embedding_api_base(api_base: str) -> str:
return normalize_openai_base_url(api_base)
@dataclass
class SearchHit:
doc_id: str
title: str
chunk: str
score: float
metadata: Dict[str, Any]
class KnowledgeIndexService:
def __init__(self) -> None:
self._lock = threading.RLock()
self._cache: Dict[str, Tuple[str, Any, List[Dict[str, Any]]]] = {}
@staticmethod
def _signature(kb: Dict[str, Any]) -> str:
doc_parts = []
for doc in kb.get("documents", []):
doc_parts.append(f"{doc.get('id')}:{doc.get('updated_at')}:{len(doc.get('content', ''))}")
return "|".join(
[
str(kb.get("updated_at")),
str(kb.get("chunk_size")),
str(kb.get("chunk_overlap")),
*doc_parts,
]
)
@staticmethod
def _fallback_chunks(kb: Dict[str, Any]) -> List[Dict[str, Any]]:
chunks: List[Dict[str, Any]] = []
chunk_size = int(kb.get("chunk_size") or 512)
overlap = int(kb.get("chunk_overlap") or 50)
step = max(1, chunk_size - overlap)
for doc in kb.get("documents", []):
text = doc.get("content") or ""
if not text:
continue
if len(text) <= chunk_size:
chunks.append(
{
"doc_id": doc.get("id", ""),
"title": doc.get("title", ""),
"chunk": text,
"metadata": doc.get("metadata") or {},
}
)
continue
for start in range(0, len(text), step):
piece = text[start : start + chunk_size]
if not piece:
continue
chunks.append(
{
"doc_id": doc.get("id", ""),
"title": doc.get("title", ""),
"chunk": piece,
"metadata": doc.get("metadata") or {},
}
)
return chunks
def _build_index(self, kb: Dict[str, Any]) -> Tuple[Any, List[Dict[str, Any]]]:
fallback_chunks = self._fallback_chunks(kb)
if not LLAMAINDEX_AVAILABLE:
return None, fallback_chunks
chunk_size = int(kb.get("chunk_size") or 512)
overlap = int(kb.get("chunk_overlap") or 50)
splitter = SentenceSplitter(chunk_size=chunk_size, chunk_overlap=overlap)
docs = [
Document(
text=(doc.get("content") or ""),
metadata={
"doc_id": doc.get("id", ""),
"title": doc.get("title", ""),
**(doc.get("metadata") or {}),
},
)
for doc in kb.get("documents", [])
if (doc.get("content") or "").strip()
]
if not docs:
return None, fallback_chunks
embed_model = self._build_embed_model(kb)
if embed_model is not None:
index = VectorStoreIndex.from_documents(
docs,
transformations=[splitter],
embed_model=embed_model,
)
else:
index = VectorStoreIndex.from_documents(docs, transformations=[splitter])
return index, fallback_chunks
@staticmethod
def _build_embed_model(kb: Dict[str, Any]) -> Any:
global_config = knowledge_global_config_store.get()
api_base = global_config.get("api_base")
api_key = global_config.get("api_key")
model_name = kb.get("embedding_model") or global_config.get("default_embedding_model")
if not api_base or not api_key or not model_name:
return None
api_base = _normalize_embedding_api_base(api_base)
try:
from llama_index.embeddings.openai_like import OpenAILikeEmbedding
return OpenAILikeEmbedding(
model_name=model_name,
api_base=api_base,
api_key=api_key,
embed_batch_size=10,
)
except Exception:
try:
from llama_index.embeddings.openai import OpenAIEmbedding
return OpenAIEmbedding(
model_name=model_name,
api_base=api_base,
api_key=api_key,
embed_batch_size=10,
)
except Exception:
return None
def reindex(self, kb_id: str) -> Dict[str, Any]:
kb = knowledge_base_store.get(kb_id)
if not kb:
raise ValueError("Knowledge base not found")
with self._lock:
signature = self._signature(kb)
index, fallback_chunks = self._build_index(kb)
self._cache[kb_id] = (signature, index, fallback_chunks)
return {
"kb_id": kb_id,
"status": "ok",
"documents": len(kb.get("documents", [])),
"engine": "llamaindex" if LLAMAINDEX_AVAILABLE and index is not None else "fallback",
}
@staticmethod
def _fallback_search(query: str, chunks: List[Dict[str, Any]], top_k: int) -> List[SearchHit]:
q_tokens = _tokenize(query)
if not q_tokens:
return []
q_set = set(q_tokens)
scored: List[SearchHit] = []
for chunk_item in chunks:
c_tokens = _tokenize(chunk_item.get("chunk", ""))
if not c_tokens:
continue
overlap = sum(1 for t in c_tokens if t in q_set)
if overlap == 0:
continue
score = overlap / math.sqrt(len(c_tokens))
scored.append(
SearchHit(
doc_id=chunk_item.get("doc_id", ""),
title=chunk_item.get("title", ""),
chunk=chunk_item.get("chunk", ""),
score=float(score),
metadata=chunk_item.get("metadata") or {},
)
)
scored.sort(key=lambda x: x.score, reverse=True)
return scored[:top_k]
def search(self, kb_id: str, query: str, top_k: int | None = None) -> Dict[str, Any]:
kb = knowledge_base_store.get(kb_id)
if not kb:
raise ValueError("Knowledge base not found")
if not kb.get("documents"):
return {"answer": "", "hits": []}
effective_top_k = int(top_k or kb.get("top_k") or 3)
with self._lock:
signature = self._signature(kb)
cached = self._cache.get(kb_id)
if not cached or cached[0] != signature:
index, fallback_chunks = self._build_index(kb)
cached = (signature, index, fallback_chunks)
self._cache[kb_id] = cached
_, index, fallback_chunks = cached
if index is None:
hits = self._fallback_search(query=query, chunks=fallback_chunks, top_k=effective_top_k)
answer = "\n\n".join(hit.chunk for hit in hits)
return {
"answer": answer,
"hits": [hit.__dict__ for hit in hits],
}
retriever = index.as_retriever(similarity_top_k=effective_top_k)
response_nodes = retriever.retrieve(query)
hits: List[Dict[str, Any]] = []
for node_with_score in response_nodes:
node = getattr(node_with_score, "node", None)
metadata = getattr(node, "metadata", {}) if node is not None else {}
chunk_text = ""
if node is not None and hasattr(node, "get_content"):
chunk_text = node.get_content()
elif node is not None:
chunk_text = str(getattr(node, "text", ""))
hits.append(
{
"doc_id": metadata.get("doc_id", ""),
"title": metadata.get("title", ""),
"chunk": chunk_text,
"score": float(getattr(node_with_score, "score", 0.0) or 0.0),
"metadata": metadata,
}
)
if not hits:
fallback_hits = self._fallback_search(query=query, chunks=fallback_chunks, top_k=effective_top_k)
return {
"answer": "\n\n".join(hit.chunk for hit in fallback_hits),
"hits": [hit.__dict__ for hit in fallback_hits],
}
answer = "\n\n".join(item.get("chunk", "") for item in hits if item.get("chunk"))
return {"answer": answer, "hits": hits}
knowledge_index_service = KnowledgeIndexService()
+5
View File
@@ -0,0 +1,5 @@
def normalize_openai_base_url(api_base: str) -> str:
normalized = (api_base or "").strip().rstrip("/")
if normalized.lower().endswith("/embeddings"):
normalized = normalized[: -len("/embeddings")]
return normalized
+59
View File
@@ -0,0 +1,59 @@
import json
from typing import Any
from nanobot.agent.tools.base import Tool
from app.context import current_knowledge_base_id
from app.services.knowledge_index import knowledge_index_service
class KnowledgeBaseRetrieveTool(Tool):
@property
def name(self) -> str:
return "knowledge_retrieve"
@property
def description(self) -> str:
return "Retrieve relevant context from the selected knowledge base to answer user questions."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "User question or retrieval query.",
},
"knowledge_base_id": {
"type": "string",
"description": "Optional knowledge base id, defaults to current session setting.",
},
"top_k": {
"type": "integer",
"description": "Maximum number of returned chunks.",
"minimum": 1,
"maximum": 20,
},
},
"required": ["query"],
}
async def execute(self, **kwargs: Any) -> str:
query = (kwargs.get("query") or "").strip()
if not query:
return "Query is required."
kb_id = (kwargs.get("knowledge_base_id") or current_knowledge_base_id.get() or "").strip()
if not kb_id:
return "No knowledge base is selected in this session."
top_k = kwargs.get("top_k")
try:
result = knowledge_index_service.search(kb_id=kb_id, query=query, top_k=top_k)
except ValueError as exc:
return str(exc)
payload = {
"knowledge_base_id": kb_id,
"answer": result.get("answer", ""),
"hits": result.get("hits", []),
}
return json.dumps(payload, ensure_ascii=False)
+110 -4
View File
@@ -16,7 +16,7 @@ import re
import os
from datetime import datetime
from app.api import upload, llm, skills, users, datasources, projects, semantic, mcp, subagents
from app.api import upload, llm, skills, users, datasources, projects, semantic, mcp, subagents, knowledge
from app.connectors.postgres import postgres_connector
from app.connectors.clickhouse import clickhouse_connector
from app.core.artifacts import extract_artifacts
@@ -24,7 +24,15 @@ from app.core.data_root import ensure_data_layout, get_data_root, get_reports_ro
from app.core.files import ensure_artifact_access, resolve_artifact_target
from app.core.nanobot import nanobot_service
from app.core.session_alias_store import session_alias_store
from app.context import current_session_id, current_progress_callback, current_viz_data, current_data_source, current_file_url
from app.context import (
current_session_id,
current_progress_callback,
current_viz_data,
current_data_source,
current_file_url,
current_knowledge_base_id,
)
from app.services.knowledge_index import knowledge_index_service
from app.database import engine, Base
# Import all models to ensure they are registered
from app.models.user import User
@@ -62,6 +70,7 @@ app.include_router(datasources.router, prefix="/api/v1")
app.include_router(semantic.router, prefix="/api/v1")
app.include_router(mcp.router, prefix="/api/v1")
app.include_router(subagents.router, prefix="/api/v1")
app.include_router(knowledge.router, prefix="/api/v1")
STREAM_DELTA_CHUNK_SIZE = 48
PREVIEWABLE_TEXT_EXTENSIONS = {
@@ -221,6 +230,7 @@ class ChatRequest(BaseModel):
prefer_sql_chart: bool = False
file_url: Optional[str] = None
route_mode: Literal["auto", "chat", "sql"] = "auto"
knowledge_base_id: Optional[str] = None
def _session_context_for_routing(session_id: str) -> Dict[str, Any]:
@@ -240,6 +250,53 @@ def _resolve_effective_source(request: ChatRequest) -> str:
return effective_source
def _resolve_effective_knowledge_base_id(request: ChatRequest) -> Optional[str]:
if request.knowledge_base_id:
return request.knowledge_base_id
session_ctx = _session_context_for_routing(request.session_id)
kb_id = session_ctx.get("selected_knowledge_base_id")
if isinstance(kb_id, str) and kb_id.strip():
return kb_id
return None
def _extract_kb_citations(kb_id: Optional[str], message: str) -> Tuple[str, List[Dict[str, Any]]]:
if not kb_id:
return message, []
try:
result = knowledge_index_service.search(kb_id=kb_id, query=message, top_k=3)
hits = result.get("hits", []) if isinstance(result, dict) else []
if not isinstance(hits, list) or not hits:
return f"[System: A knowledge base is selected ({kb_id}). Retrieval result is empty.]\n{message}", []
lines: List[str] = []
citations: List[Dict[str, Any]] = []
for idx, item in enumerate(hits[:3], start=1):
if not isinstance(item, dict):
continue
title = str(item.get("title") or f"Doc {idx}")
chunk = str(item.get("chunk") or "").strip()
if not chunk:
continue
score = float(item.get("score", 0.0) or 0.0)
lines.append(f"[{idx}] {title}\n{chunk}")
citations.append(
{
"doc_id": str(item.get("doc_id") or ""),
"title": title,
"score": round(score, 4),
"chunk": chunk[:360],
"metadata": item.get("metadata") or {},
}
)
if not lines:
return f"[System: A knowledge base is selected ({kb_id}). Retrieval result is empty.]\n{message}", []
context_block = "\n\n".join(lines)
next_message = f"[System: The following context is retrieved from knowledge base {kb_id}. You must ground your answer on it when relevant.]\n{context_block}\n\n{message}"
return next_message, citations
except Exception as exc:
return f"[System: A knowledge base is selected ({kb_id}) but retrieval failed: {exc}]\n{message}", []
def _sync_session_project(session_id: str, project_id: Optional[int]) -> None:
if project_id is None:
return
@@ -248,6 +305,25 @@ def _sync_session_project(session_id: str, project_id: Optional[int]) -> None:
project_id=project_id,
)
def _sync_session_chat_context(
session_id: str,
selected_data_source: Optional[str] = None,
selected_knowledge_base_id: Optional[str] = None,
) -> None:
if not nanobot_service.agent:
return
sessions = nanobot_service.agent.sessions
session = sessions.get_or_create(session_id)
if selected_data_source:
session.metadata["selected_data_source"] = selected_data_source
if selected_knowledge_base_id:
session.metadata["selected_knowledge_base_id"] = selected_knowledge_base_id
session.updated_at = datetime.now()
save_fn = getattr(sessions, "save", None)
if callable(save_fn):
save_fn(session)
class SessionAliasUpdateRequest(BaseModel):
title: Optional[str] = None
pinned: Optional[bool] = None
@@ -262,6 +338,7 @@ class BatchDeleteRequest(BaseModel):
class SessionFileContextUpdateRequest(BaseModel):
active_data_file: Optional[Dict[str, Any]] = None
selected_data_source: Optional[str] = None
selected_knowledge_base_id: Optional[str] = None
def _persist_assistant_enrichment(
@@ -269,6 +346,7 @@ def _persist_assistant_enrichment(
viz_payload: Optional[Dict[str, Any]] = None,
artifacts: Optional[List[Dict[str, Any]]] = None,
usage: Optional[Dict[str, Any]] = None,
kb_citations: Optional[List[Dict[str, Any]]] = None,
) -> None:
if not nanobot_service.agent:
return
@@ -285,6 +363,9 @@ def _persist_assistant_enrichment(
if usage:
session.messages[-1]["usage"] = usage
changed = True
if kb_citations is not None:
session.messages[-1]["kb_citations"] = kb_citations
changed = True
if changed:
nanobot_service.agent.sessions.save(session)
@@ -306,13 +387,20 @@ async def nanobot_chat(request: ChatRequest):
try:
_sync_session_project(request.session_id, request.project_id)
resolved_source = _resolve_effective_source(request)
resolved_kb_id = _resolve_effective_knowledge_base_id(request)
_sync_session_chat_context(
session_id=request.session_id,
selected_data_source=resolved_source,
selected_knowledge_base_id=resolved_kb_id,
)
current_data_source.set(resolved_source)
current_file_url.set(request.file_url)
current_knowledge_base_id.set(resolved_kb_id)
current_session_id.set(request.session_id)
current_viz_data.set({})
# Inject instructions if explicitly routed
message = request.message
message, kb_citations = _extract_kb_citations(resolved_kb_id, request.message)
if request.route_mode == "sql" or request.prefer_sql_chart:
message = f"[System: Use the nl2sql tool to answer the query]\n{message}"
elif request.route_mode == "chat":
@@ -344,6 +432,7 @@ async def nanobot_chat(request: ChatRequest):
viz_payload=viz_payload if isinstance(viz_payload, dict) else None,
artifacts=artifacts,
usage=usage,
kb_citations=kb_citations,
)
payload = {
@@ -355,6 +444,8 @@ async def nanobot_chat(request: ChatRequest):
payload["artifacts"] = artifacts
if usage:
payload["usage"] = usage
if kb_citations:
payload["kb_citations"] = kb_citations
return payload
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -366,8 +457,15 @@ async def nanobot_chat_stream(request: ChatRequest):
try:
_sync_session_project(request.session_id, request.project_id)
resolved_source = _resolve_effective_source(request)
resolved_kb_id = _resolve_effective_knowledge_base_id(request)
_sync_session_chat_context(
session_id=request.session_id,
selected_data_source=resolved_source,
selected_knowledge_base_id=resolved_kb_id,
)
current_data_source.set(resolved_source)
current_file_url.set(request.file_url)
current_knowledge_base_id.set(resolved_kb_id)
current_session_id.set(request.session_id)
current_viz_data.set({})
@@ -388,7 +486,7 @@ async def nanobot_chat_stream(request: ChatRequest):
current_progress_callback.set(_on_progress)
# Inject instructions if explicitly routed
message = request.message
message, kb_citations = _extract_kb_citations(resolved_kb_id, request.message)
if request.route_mode == "sql" or request.prefer_sql_chart:
message = f"[System: Use the nl2sql tool to answer the query]\n{message}"
elif request.route_mode == "chat":
@@ -472,6 +570,7 @@ async def nanobot_chat_stream(request: ChatRequest):
viz_payload=viz_payload if isinstance(viz_payload, dict) else None,
artifacts=artifacts,
usage=usage,
kb_citations=kb_citations,
)
final_payload = {"type": "final", "content": text}
@@ -481,6 +580,8 @@ async def nanobot_chat_stream(request: ChatRequest):
final_payload["artifacts"] = artifacts
if usage:
final_payload["usage"] = usage
if kb_citations:
final_payload["kb_citations"] = kb_citations
yield f"data: {json.dumps(final_payload, ensure_ascii=False)}\n\n"
yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
except asyncio.CancelledError:
@@ -619,6 +720,11 @@ def update_session_context_file(session_id: str, payload: SessionFileContextUpda
session.metadata["selected_data_source"] = payload.selected_data_source
else:
session.metadata.pop("selected_data_source", None)
if "selected_knowledge_base_id" in updated_fields:
if payload.selected_knowledge_base_id:
session.metadata["selected_knowledge_base_id"] = payload.selected_knowledge_base_id
else:
session.metadata.pop("selected_knowledge_base_id", None)
session.updated_at = datetime.now()
nanobot_service.agent.sessions.save(session)
return {"status": "success", "metadata": session.metadata}
+3
View File
@@ -16,6 +16,9 @@ dependencies = [
"json-repair>=0.57.0,<1.0.0",
"lark-oapi>=1.5.0,<2.0.0",
"loguru>=0.7.3,<1.0.0",
"llama-index-core>=0.14.0",
"llama-index-embeddings-openai>=0.5.1",
"llama-index-embeddings-openai-like>=0.3.1",
"mcp>=1.26.0,<2.0.0",
"msgpack>=1.1.0,<2.0.0",
"nanobot-ai",
@@ -0,0 +1,376 @@
import asyncio
import sys
from pathlib import Path
BACKEND_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = BACKEND_ROOT.parent
NANOBOT_ROOT = REPO_ROOT / "nanobot"
if str(BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(BACKEND_ROOT))
if str(NANOBOT_ROOT) not in sys.path:
sys.path.insert(0, str(NANOBOT_ROOT))
from fastapi.testclient import TestClient
import main
from app.context import current_knowledge_base_id
from app.schemas.knowledge import KnowledgeSearchResponse
from app.tools.knowledge_base import KnowledgeBaseRetrieveTool
def test_knowledge_base_crud_and_search_routes(monkeypatch, tmp_path) -> None:
async def fake_start():
return None
async def fake_stop():
return None
monkeypatch.setenv("DATA_ROOT", str(tmp_path))
monkeypatch.setattr(main.nanobot_service, "start", fake_start)
monkeypatch.setattr(main.nanobot_service, "stop", fake_stop)
client = TestClient(main.app)
create_resp = client.post(
"/api/v1/knowledge-bases",
json={"name": "产品手册", "description": "用于问答", "top_k": 2, "chunk_size": 256, "chunk_overlap": 20},
)
assert create_resp.status_code == 200
kb = create_resp.json()
kb_id = kb["id"]
list_resp = client.get("/api/v1/knowledge-bases")
assert list_resp.status_code == 200
assert any(item["id"] == kb_id for item in list_resp.json())
doc_resp = client.post(
f"/api/v1/knowledge-bases/{kb_id}/documents",
json={"title": "退款规则", "content": "苹果手机支持7天无理由退款", "metadata": {"lang": "zh"}},
)
assert doc_resp.status_code == 200
doc_id = doc_resp.json()["id"]
reindex_resp = client.post(f"/api/v1/knowledge-bases/{kb_id}/reindex")
assert reindex_resp.status_code == 200
search_resp = client.post(f"/api/v1/knowledge-bases/{kb_id}/search", json={"query": "苹果退款", "top_k": 2})
assert search_resp.status_code == 200
parsed = KnowledgeSearchResponse(**search_resp.json())
assert parsed.hits
assert "苹果" in parsed.answer
update_resp = client.put(f"/api/v1/knowledge-bases/{kb_id}", json={"name": "售后知识库"})
assert update_resp.status_code == 200
assert update_resp.json()["name"] == "售后知识库"
delete_doc_resp = client.delete(f"/api/v1/knowledge-bases/{kb_id}/documents/{doc_id}")
assert delete_doc_resp.status_code == 200
delete_kb_resp = client.delete(f"/api/v1/knowledge-bases/{kb_id}")
assert delete_kb_resp.status_code == 200
def test_knowledge_global_config_mask_and_validation(monkeypatch, tmp_path) -> None:
async def fake_start():
return None
async def fake_stop():
return None
monkeypatch.setenv("DATA_ROOT", str(tmp_path))
monkeypatch.setattr(main.nanobot_service, "start", fake_start)
monkeypatch.setattr(main.nanobot_service, "stop", fake_stop)
client = TestClient(main.app)
initial_resp = client.get("/api/v1/knowledge-bases/global-config")
assert initial_resp.status_code == 200
assert initial_resp.json() == {
"api_base": None,
"api_key": None,
"api_key_masked": None,
"has_api_key": False,
"default_embedding_model": None,
}
update_resp = client.put(
"/api/v1/knowledge-bases/global-config",
json={"api_base": "https://kb.example.com/", "api_key": "sk-knowledge-secret", "default_embedding_model": "text-embedding-3-small"},
)
assert update_resp.status_code == 200
body = update_resp.json()
assert body["api_base"] == "https://kb.example.com"
assert body["api_key"] is None
assert body["has_api_key"] is True
assert body["api_key_masked"] == "sk-k***********cret"
assert body["default_embedding_model"] == "text-embedding-3-small"
get_resp = client.get("/api/v1/knowledge-bases/global-config")
assert get_resp.status_code == 200
assert get_resp.json()["api_key"] is None
assert get_resp.json()["api_key_masked"] == "sk-k***********cret"
assert get_resp.json()["default_embedding_model"] == "text-embedding-3-small"
invalid_resp = client.put("/api/v1/knowledge-bases/global-config", json={"api_base": "ftp://kb.example.com"})
assert invalid_resp.status_code == 422
def test_chat_request_syncs_knowledge_base_metadata(monkeypatch) -> None:
captured_kb_ids: list[str | None] = []
captured_messages: list[str] = []
class _DummySession:
def __init__(self):
self.metadata = {}
self.messages = []
self.updated_at = None
class _DummySessions:
def __init__(self):
self._sessions: dict[str, _DummySession] = {}
def get_or_create(self, key: str):
if key not in self._sessions:
self._sessions[key] = _DummySession()
return self._sessions[key]
def save(self, _session):
return None
class _DummyAgent:
def __init__(self):
self.sessions = _DummySessions()
async def fake_process_message(*args, **kwargs):
captured_kb_ids.append(current_knowledge_base_id.get())
if args and isinstance(args[0], str):
captured_messages.append(args[0])
return "ok"
def fake_search(*, kb_id: str, query: str, top_k=None):
assert kb_id == "kb-123"
assert query == "请回答售后规则"
return {
"answer": "命中结果",
"hits": [
{"doc_id": "d1", "title": "退款规则", "chunk": "7天无理由退款", "score": 0.9, "metadata": {}},
{"doc_id": "d2", "title": "售后电话", "chunk": "客服电话 400-1234", "score": 0.7, "metadata": {}},
],
}
monkeypatch.setattr(main.nanobot_service, "agent", _DummyAgent())
monkeypatch.setattr(main.nanobot_service, "process_message", fake_process_message)
monkeypatch.setattr("main.knowledge_index_service.search", fake_search)
request = main.ChatRequest(message="请回答售后规则", session_id="api:kb-1", knowledge_base_id="kb-123")
response = asyncio.run(main.nanobot_chat(request))
assert response["response"] == "ok"
assert "kb_citations" in response
assert len(response["kb_citations"]) == 2
assert response["kb_citations"][0]["title"] == "退款规则"
assert captured_kb_ids == ["kb-123"]
assert captured_messages and "7天无理由退款" in captured_messages[0]
session = main.nanobot_service.agent.sessions.get_or_create("api:kb-1")
assert session.metadata["selected_knowledge_base_id"] == "kb-123"
def test_knowledge_tool_uses_session_context(monkeypatch) -> None:
tool = KnowledgeBaseRetrieveTool()
token = current_knowledge_base_id.set("kb-session")
called: list[dict] = []
def fake_search(*, kb_id: str, query: str, top_k=None):
called.append({"kb_id": kb_id, "query": query, "top_k": top_k})
return {
"answer": "命中结果",
"hits": [{"doc_id": "d1", "title": "t1", "chunk": "命中结果", "score": 1.0, "metadata": {}}],
}
monkeypatch.setattr("app.tools.knowledge_base.knowledge_index_service.search", fake_search)
try:
output = asyncio.run(tool.execute(query="售后政策"))
finally:
current_knowledge_base_id.reset(token)
assert called and called[0]["kb_id"] == "kb-session"
assert "命中结果" in output
def test_update_session_context_file_supports_knowledge_base(monkeypatch) -> None:
class _DummySession:
def __init__(self):
self.metadata = {}
self.updated_at = None
class _DummySessions:
def __init__(self):
self.session = _DummySession()
def get_or_create(self, _key: str):
return self.session
def save(self, _session):
return None
class _DummyAgent:
def __init__(self):
self.sessions = _DummySessions()
monkeypatch.setattr(main.nanobot_service, "agent", _DummyAgent())
payload = main.SessionFileContextUpdateRequest(selected_knowledge_base_id="kb-ctx")
response = main.update_session_context_file("api:ctx", payload)
assert response["status"] == "success"
assert response["metadata"]["selected_knowledge_base_id"] == "kb-ctx"
def test_knowledge_global_connection_test_route(monkeypatch, tmp_path) -> None:
async def fake_start():
return None
async def fake_stop():
return None
class _DummyEmbeddingData:
embedding = [0.1, 0.2, 0.3]
class _DummyEmbeddingResp:
data = [_DummyEmbeddingData()]
class _DummyEmbeddingsAPI:
@staticmethod
def create(model: str, input: str):
assert model == "text-embedding-3-small"
assert input == "connection test"
return _DummyEmbeddingResp()
class _DummyOpenAI:
def __init__(self, api_key: str, base_url: str):
assert api_key == "sk-knowledge-secret"
assert base_url == "https://kb.example.com"
self.embeddings = _DummyEmbeddingsAPI()
monkeypatch.setenv("DATA_ROOT", str(tmp_path))
monkeypatch.setattr(main.nanobot_service, "start", fake_start)
monkeypatch.setattr(main.nanobot_service, "stop", fake_stop)
monkeypatch.setattr("app.api.knowledge.OpenAI", _DummyOpenAI)
client = TestClient(main.app)
save_resp = client.put(
"/api/v1/knowledge-bases/global-config",
json={
"api_base": "https://kb.example.com",
"api_key": "sk-knowledge-secret",
"default_embedding_model": "text-embedding-3-small",
},
)
assert save_resp.status_code == 200
test_resp = client.post(
"/api/v1/knowledge-bases/global-config/test-connection",
json={"model_name": "text-embedding-3-small"},
)
assert test_resp.status_code == 200
body = test_resp.json()
assert body["success"] is True
assert body["model_name"] == "text-embedding-3-small"
assert body["embedding_dimension"] == 3
assert body["resolved_api_base"] == "https://kb.example.com"
assert body["available_models"] == []
def test_knowledge_global_connection_test_route_requires_model_name(monkeypatch, tmp_path) -> None:
async def fake_start():
return None
async def fake_stop():
return None
monkeypatch.setenv("DATA_ROOT", str(tmp_path))
monkeypatch.setattr(main.nanobot_service, "start", fake_start)
monkeypatch.setattr(main.nanobot_service, "stop", fake_stop)
client = TestClient(main.app)
resp = client.post(
"/api/v1/knowledge-bases/global-config/test-connection",
json={
"api_base": "https://api.siliconflow.cn/v1/embeddings",
"api_key": "ark-key",
},
)
assert resp.status_code == 400
assert "测试连接必须显式填写向量模型名称" in resp.json()["detail"]
def test_knowledge_global_connection_test_route_returns_remote_error(monkeypatch, tmp_path) -> None:
async def fake_start():
return None
async def fake_stop():
return None
class _DummyEmbeddingsAPI:
@staticmethod
def create(model: str, input: str):
assert model == "BAAI/bge-large-zh-v1.5"
assert input == "connection test"
raise RuntimeError("Not Found")
class _DummyOpenAI:
def __init__(self, api_key: str, base_url: str):
assert api_key == "sf-key"
assert base_url == "https://api.siliconflow.cn/v1"
self.embeddings = _DummyEmbeddingsAPI()
monkeypatch.setenv("DATA_ROOT", str(tmp_path))
monkeypatch.setattr(main.nanobot_service, "start", fake_start)
monkeypatch.setattr(main.nanobot_service, "stop", fake_stop)
monkeypatch.setattr("app.api.knowledge.OpenAI", _DummyOpenAI)
client = TestClient(main.app)
resp = client.post(
"/api/v1/knowledge-bases/global-config/test-connection",
json={
"api_base": "https://api.siliconflow.cn/v1/embeddings",
"api_key": "sf-key",
"model_name": "BAAI/bge-large-zh-v1.5",
},
)
assert resp.status_code == 400
assert "Embedding调用失败" in resp.json()["detail"]
def test_knowledge_document_upload_route(monkeypatch, tmp_path) -> None:
async def fake_start():
return None
async def fake_stop():
return None
monkeypatch.setenv("DATA_ROOT", str(tmp_path))
monkeypatch.setattr(main.nanobot_service, "start", fake_start)
monkeypatch.setattr(main.nanobot_service, "stop", fake_stop)
client = TestClient(main.app)
create_resp = client.post(
"/api/v1/knowledge-bases",
json={"name": "上传测试库", "description": "用于上传", "top_k": 2, "chunk_size": 256, "chunk_overlap": 20},
)
assert create_resp.status_code == 200
kb_id = create_resp.json()["id"]
files = [
("files", ("doc1.txt", b"hello knowledge", "text/plain")),
("files", ("doc2.md", b"# title\ncontent", "text/markdown")),
]
upload_resp = client.post(
f"/api/v1/knowledge-bases/{kb_id}/documents/upload",
files=files,
data={"metadata": "{\"source\":\"batch\"}"},
)
assert upload_resp.status_code == 200
body = upload_resp.json()
assert body["status"] == "success"
assert body["count"] == 2
assert len(body["documents"]) == 2
list_resp = client.get(f"/api/v1/knowledge-bases/{kb_id}/documents")
assert list_resp.status_code == 200
docs = list_resp.json()
assert len(docs) == 2
+575
View File
@@ -134,6 +134,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
[[package]]
name = "aiosqlite"
version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" },
]
[[package]]
name = "annotated-doc"
version = "0.0.4"
@@ -220,6 +229,9 @@ dependencies = [
{ name = "httpx" },
{ name = "json-repair" },
{ name = "lark-oapi" },
{ name = "llama-index-core" },
{ name = "llama-index-embeddings-openai" },
{ name = "llama-index-embeddings-openai-like" },
{ name = "loguru" },
{ name = "mcp" },
{ name = "msgpack" },
@@ -264,6 +276,9 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.28.0,<1.0.0" },
{ name = "json-repair", specifier = ">=0.57.0,<1.0.0" },
{ name = "lark-oapi", specifier = ">=1.5.0,<2.0.0" },
{ name = "llama-index-core", specifier = ">=0.14.0" },
{ name = "llama-index-embeddings-openai", specifier = ">=0.5.1" },
{ name = "llama-index-embeddings-openai-like", specifier = ">=0.3.1" },
{ name = "loguru", specifier = ">=0.7.3,<1.0.0" },
{ name = "mcp", specifier = ">=1.26.0,<2.0.0" },
{ name = "msgpack", specifier = ">=1.1.0,<2.0.0" },
@@ -296,6 +311,23 @@ requires-dist = [
{ name = "websockets", specifier = ">=16.0,<17.0" },
]
[[package]]
name = "banks"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "filetype" },
{ name = "griffe" },
{ name = "jinja2" },
{ name = "platformdirs" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/5d/54c79aaaa9aa1278af24cae98d81d6ef635ad840f046bc2ccb5041ddeb1b/banks-2.4.1.tar.gz", hash = "sha256:8cbf1553f14c44d4f7e9c2064ad9212ce53ee4da000b2f8308d548b60db56655", size = 188033, upload-time = "2026-02-17T11:21:14.855Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/5a/f38b49e8b225b0c774e97c9495e52ab9ccdf6d82bde68c513bd736820eb2/banks-2.4.1-py3-none-any.whl", hash = "sha256:40e6d9b6e9b69fb403fa31f2853b3297e4919c1b6f2179b2119d2d4473c6ed13", size = 35032, upload-time = "2026-02-17T11:21:13.236Z" },
]
[[package]]
name = "bcrypt"
version = "5.0.0"
@@ -702,6 +734,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/0c/7bb51e3acfafd16c48875bf3db03607674df16f5b6ef8d056586af7e2b8b/cssselect-1.4.0-py3-none-any.whl", hash = "sha256:c0ec5c0191c8ee39fcc8afc1540331d8b55b0183478c50e9c8a79d44dbceb1d8", size = 18540, upload-time = "2026-01-29T07:00:24.994Z" },
]
[[package]]
name = "dataclasses-json"
version = "0.6.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "marshmallow" },
{ name = "typing-inspect" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" },
]
[[package]]
name = "ddgs"
version = "9.12.0"
@@ -716,6 +761,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/8f/c5229d519af06a1405ad83bf4d0429d5d3c29b7c9dc51a96d2afba26bdeb/ddgs-9.12.0-py3-none-any.whl", hash = "sha256:54f24abdff538e8f9b83f99af99455776021419b704b11d796b17825f8baff1a", size = 45452, upload-time = "2026-03-27T16:16:03.677Z" },
]
[[package]]
name = "deprecated"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
[[package]]
name = "dingtalk-stream"
version = "0.24.3"
@@ -729,6 +786,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/44/102dede3f371277598df6aa9725b82e3add068c729333c7a5dbc12764579/dingtalk_stream-0.24.3-py3-none-any.whl", hash = "sha256:2160403656985962878bf60cdf5adf41619f21067348e06f07a7c7eebf5943ad", size = 27813, upload-time = "2025-10-24T09:36:57.497Z" },
]
[[package]]
name = "dirtyjson"
version = "1.0.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782, upload-time = "2022-11-28T23:32:33.319Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
@@ -820,6 +886,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" },
]
[[package]]
name = "filetype"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" },
]
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -925,6 +1000,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
]
[[package]]
name = "fsspec"
version = "2026.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" },
]
[[package]]
name = "greenlet"
version = "3.3.2"
@@ -977,6 +1061,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" },
]
[[package]]
name = "griffe"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffecli" },
{ name = "griffelib" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4a/49/eb6d2935e27883af92c930ed40cc4c69bcd32c402be43b8ca4ab20510f67/griffe-2.0.2.tar.gz", hash = "sha256:c5d56326d159f274492e9bf93a9895cec101155d944caa66d0fc4e0c13751b92", size = 293757, upload-time = "2026-03-27T11:34:52.205Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/c0/2bb018eecf9a83c68db9cd9fffd9dab25f102ad30ed869451046e46d1187/griffe-2.0.2-py3-none-any.whl", hash = "sha256:2b31816460aee1996af26050a1fc6927a2e5936486856707f55508e4c9b5960b", size = 5141, upload-time = "2026-03-27T11:34:47.721Z" },
]
[[package]]
name = "griffecli"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
{ name = "griffelib" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/e0/6a7d661d71bb043656a109b91d84a42b5342752542074ec83b16a6eb97f0/griffecli-2.0.2.tar.gz", hash = "sha256:40a1ad4181fc39685d025e119ae2c5b669acdc1f19b705fb9bf971f4e6f6dffb", size = 56281, upload-time = "2026-03-27T11:34:50.087Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/e8/90d93356c88ac34c20cb5edffca68138df55ca9bbd1a06eccfbcec8fdbe5/griffecli-2.0.2-py3-none-any.whl", hash = "sha256:0d44d39e59afa81e288a3e1c3bf352cc4fa537483326ac06b8bb6a51fd8303a0", size = 9500, upload-time = "2026-03-27T11:34:48.81Z" },
]
[[package]]
name = "griffelib"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -1037,6 +1156,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "jiter"
version = "0.13.0"
@@ -1122,6 +1253,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
]
[[package]]
name = "joblib"
version = "1.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
[[package]]
name = "json-repair"
version = "0.58.5"
@@ -1173,6 +1313,97 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/ff/2ece5d735ebfa2af600a53176f2636ae47af2bf934e08effab64f0d1e047/lark_oapi-1.5.3-py3-none-any.whl", hash = "sha256:fda6b32bb38d21b6bdaae94979c600b94c7c521e985adade63a54e4b3e20cc36", size = 6993016, upload-time = "2026-01-27T08:21:49.307Z" },
]
[[package]]
name = "llama-index-core"
version = "0.14.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "aiosqlite" },
{ name = "banks" },
{ name = "dataclasses-json" },
{ name = "deprecated" },
{ name = "dirtyjson" },
{ name = "filetype" },
{ name = "fsspec" },
{ name = "httpx" },
{ name = "llama-index-workflows" },
{ name = "nest-asyncio" },
{ name = "networkx" },
{ name = "nltk" },
{ name = "numpy" },
{ name = "pillow" },
{ name = "platformdirs" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "setuptools" },
{ name = "sqlalchemy", extra = ["asyncio"] },
{ name = "tenacity" },
{ name = "tiktoken" },
{ name = "tinytag" },
{ name = "tqdm" },
{ name = "typing-extensions" },
{ name = "typing-inspect" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/eb/a661cc2f70177f59cfe7bfcdb7a4e9352fb073ab46927068151bf2905fbb/llama_index_core-0.14.19.tar.gz", hash = "sha256:7b17f321f0d965495402890991b2bfde49d4197bc46ca5970300cc7b9c2df6a2", size = 11599592, upload-time = "2026-03-25T20:58:25.751Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/b6/6c2678b8597903503b804fe831a203d299bcbcc07bdf35789a484e67f7c0/llama_index_core-0.14.19-py3-none-any.whl", hash = "sha256:807352f16a300f9980d0110cfdaa81d07e201384965e9f7d940c8ead80d463ed", size = 11945679, upload-time = "2026-03-25T20:58:28.265Z" },
]
[[package]]
name = "llama-index-embeddings-openai"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "llama-index-core" },
{ name = "openai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/52/eb56a4887501651fb17400f7f571c1878109ff698efbe0bbac9165a5603d/llama_index_embeddings_openai-0.6.0.tar.gz", hash = "sha256:eb3e6606be81cb89125073e23c97c0a6119dabb4827adbd14697c2029ad73f29", size = 7629, upload-time = "2026-03-12T20:21:27.234Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/d1/4bb0b80f4057903110060f617ef519197194b3ff5dd6153d850c8f5676fa/llama_index_embeddings_openai-0.6.0-py3-none-any.whl", hash = "sha256:039bb1007ad4267e25ddb89a206dfdab862bfb87d58da4271a3919e4f9df4d61", size = 7666, upload-time = "2026-03-12T20:21:28.079Z" },
]
[[package]]
name = "llama-index-embeddings-openai-like"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "llama-index-embeddings-openai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b2/df/79e4748196213b55931d5f8377141fff41135f5988d5501860824cc95390/llama_index_embeddings_openai_like-0.3.1.tar.gz", hash = "sha256:cef7af4bce284e8e6730532dbd0aa325e77398a5d5524edb2d2e3acb122fb5b6", size = 3854, upload-time = "2026-03-13T16:15:20.647Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/8e/b9ea889f88318f2faa20b615989e12a15a133c9273630f9266fcf69f35a6/llama_index_embeddings_openai_like-0.3.1-py3-none-any.whl", hash = "sha256:167c7e462cde7d53ea907ceaffbbf10a750676c7c9f7bcc9bc9686a41921387a", size = 3631, upload-time = "2026-03-13T16:15:19.58Z" },
]
[[package]]
name = "llama-index-instrumentation"
version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/d0/671b23ccff255c9bce132a84ffd5a6f4541ceefdeab9c1786b08c9722f2e/llama_index_instrumentation-0.5.0.tar.gz", hash = "sha256:eeb724648b25d149de882a5ac9e21c5acb1ce780da214bda2b075341af29ad8e", size = 43831, upload-time = "2026-03-12T20:17:06.742Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/45/6dcaccef44e541ffa138e4b45e33e0d40ab2a7d845338483954fcf77bc75/llama_index_instrumentation-0.5.0-py3-none-any.whl", hash = "sha256:aaab83cddd9dd434278891012d8995f47a3bc7ed1736a371db90965348c56a21", size = 16444, upload-time = "2026-03-12T20:17:05.957Z" },
]
[[package]]
name = "llama-index-workflows"
version = "2.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "llama-index-instrumentation" },
{ name = "pydantic" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/a8/2198a81d96394686f598857d12164256ce11699b99a76fdbaf38b8bc1a2c/llama_index_workflows-2.17.1.tar.gz", hash = "sha256:c62fabe509cf0003ddfe5b2b27f48b3443c7c9a84e9cdc904c6f9ed5f8cbe25d", size = 86723, upload-time = "2026-03-20T15:45:14.216Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/28/20dc2db83adc2d9a11e042eac568f52788eb850e9381ffb1087d51f46672/llama_index_workflows-2.17.1-py3-none-any.whl", hash = "sha256:0d78fc36c2ab5430887c9f34367d59d4c22cf1e6c40ecdc3596214234c2b5010", size = 110539, upload-time = "2026-03-20T15:45:15.341Z" },
]
[[package]]
name = "loguru"
version = "0.7.3"
@@ -1317,6 +1548,92 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "marshmallow"
version = "3.26.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" },
]
[[package]]
name = "mcp"
version = "1.26.0"
@@ -1521,6 +1838,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "nanobot-ai"
version = "0.1.4.post6"
@@ -1604,6 +1930,39 @@ requires-dist = [
]
provides-extras = ["wecom", "weixin", "matrix", "langsmith", "dev"]
[[package]]
name = "nest-asyncio"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
[[package]]
name = "networkx"
version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
]
[[package]]
name = "nltk"
version = "3.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "joblib" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
]
[[package]]
name = "numpy"
version = "2.4.3"
@@ -1727,6 +2086,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pandas"
version = "3.0.1"
@@ -1796,6 +2164,93 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
]
[[package]]
name = "pillow"
version = "12.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
{ url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
{ url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
{ url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
{ url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
{ url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
{ url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
{ url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
{ url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
{ url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
{ url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
]
[[package]]
name = "platformdirs"
version = "4.9.4"
@@ -2705,6 +3160,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "setuptools"
version = "82.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -2827,6 +3291,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" },
]
[package.optional-dependencies]
asyncio = [
{ name = "greenlet" },
]
[[package]]
name = "sse-starlette"
version = "3.3.2"
@@ -2853,6 +3322,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
]
[[package]]
name = "tenacity"
version = "9.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
]
[[package]]
name = "tiktoken"
version = "0.12.0"
@@ -2907,6 +3385,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" },
]
[[package]]
name = "tinytag"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/59/8a8cb2331e2602b53e4dc06960f57d1387a2b18e7efd24e5f9cb60ea4925/tinytag-2.2.1.tar.gz", hash = "sha256:e6d06610ebe7cd66fd07be2d3b9495914ab32654a5e47657bb8cd44c2484523c", size = 38214, upload-time = "2026-03-15T18:48:01.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/34/d50e338631baaf65ec5396e70085e5de0b52b24b28db1ffbc1c6e82190dc/tinytag-2.2.1-py3-none-any.whl", hash = "sha256:ed8b1e6d25367937e3321e054f4974f9abfde1a3e0a538824c87da377130c2b6", size = 32927, upload-time = "2026-03-15T18:47:59.613Z" },
]
[[package]]
name = "tqdm"
version = "4.67.3"
@@ -2943,6 +3430,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspect"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
@@ -3084,6 +3584,81 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
]
[[package]]
name = "wrapt"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" },
{ url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" },
{ url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" },
{ url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" },
{ url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" },
{ url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" },
{ url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" },
{ url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" },
{ url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" },
{ url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" },
{ url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" },
{ url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" },
{ url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" },
{ url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" },
{ url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" },
{ url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" },
{ url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" },
{ url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" },
{ url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" },
{ url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" },
{ url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" },
{ url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" },
{ url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" },
{ url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" },
{ url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" },
{ url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" },
{ url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" },
{ url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" },
{ url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" },
{ url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" },
{ url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" },
{ url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" },
{ url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" },
{ url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" },
{ url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" },
{ url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" },
{ url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" },
{ url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" },
{ url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" },
{ url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" },
{ url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" },
{ url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" },
{ url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" },
{ url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" },
{ url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" },
{ url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" },
{ url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" },
{ url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" },
{ url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" },
{ url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" },
{ url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" },
{ url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" },
{ url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" },
{ url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" },
{ url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" },
{ url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" },
{ url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" },
{ url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" },
{ url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" },
{ url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" },
{ url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" },
{ url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" },
{ url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" },
]
[[package]]
name = "wsproto"
version = "1.3.2"
+191 -2
View File
@@ -31,6 +31,7 @@ interface Message {
total_tokens: number;
};
artifacts?: MessageArtifact[];
kbCitations?: KnowledgeCitation[];
}
interface MessageViz {
@@ -51,6 +52,14 @@ interface MessageArtifact {
preview_url?: string;
}
interface KnowledgeCitation {
doc_id: string;
title: string;
score: number;
chunk: string;
metadata?: Record<string, unknown>;
}
interface ArtifactPreviewTarget {
name: string;
mimeType: string;
@@ -105,6 +114,11 @@ interface Skill {
type: string;
}
interface KnowledgeBaseOption {
id: string;
name: string;
}
const dedupeSkillsById = (skills: Skill[]): Skill[] => {
const map = new Map<string, Skill>();
for (const skill of skills) {
@@ -120,6 +134,7 @@ interface SessionData {
metadata?: {
active_data_file?: DataFileContext | null;
selected_data_source?: string | null;
selected_knowledge_base_id?: string | null;
[key: string]: any;
};
messages: Array<{
@@ -179,12 +194,34 @@ const normalizeArtifacts = (raw: unknown): MessageArtifact[] => {
}, []);
};
const normalizeKnowledgeCitations = (raw: unknown): KnowledgeCitation[] => {
if (!Array.isArray(raw)) return [];
return raw.reduce<KnowledgeCitation[]>((acc, item) => {
if (!item || typeof item !== "object") return acc;
const source = item as Record<string, unknown>;
const title = typeof source.title === "string" ? source.title : "";
const chunk = typeof source.chunk === "string" ? source.chunk : "";
const score = typeof source.score === "number" ? source.score : Number(source.score || 0);
if (!title || !chunk) return acc;
acc.push({
doc_id: typeof source.doc_id === "string" ? source.doc_id : "",
title,
score: Number.isFinite(score) ? score : 0,
chunk,
metadata: source.metadata && typeof source.metadata === "object" ? source.metadata as Record<string, unknown> : undefined,
});
return acc;
}, []);
};
export function ChatInterface() {
const { t } = useTranslation();
const [messagesBySession, setMessagesBySession] = useState<Record<string, Message[]>>({});
const [input, setInput] = useState("");
const [selectedDataSource, setSelectedDataSource] = useState<string>("");
const [selectedKnowledgeBaseId, setSelectedKnowledgeBaseId] = useState<string>("");
const [availableSkills, setAvailableSkills] = useState<Skill[]>([]);
const [availableKnowledgeBases, setAvailableKnowledgeBases] = useState<KnowledgeBaseOption[]>([]);
const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [artifactPreview, setArtifactPreview] = useState<ArtifactPreviewTarget | null>(null);
@@ -443,6 +480,10 @@ export function ChatInterface() {
useEffect(() => {
if (currentProject) {
fetchDataSources();
fetchKnowledgeBases();
} else {
setAvailableKnowledgeBases([]);
setSelectedKnowledgeBaseId("");
}
}, [currentProject]);
@@ -461,9 +502,25 @@ export function ChatInterface() {
}
};
const fetchKnowledgeBases = async () => {
if (!currentProject) return;
try {
const data = await api.get<Array<{ id: string; name: string }>>(`/api/v1/knowledge-bases?project_id=${currentProject.id}`);
const projectKnowledgeBases = (data || []).map((item) => ({ id: item.id, name: item.name }));
setAvailableKnowledgeBases(projectKnowledgeBases);
if (selectedKnowledgeBaseId && !projectKnowledgeBases.find((item) => item.id === selectedKnowledgeBaseId)) {
setSelectedKnowledgeBaseId("");
void syncSessionContext({ selected_knowledge_base_id: null });
}
} catch (e) {
console.error("Failed to fetch knowledge bases", e);
}
};
const syncSessionContext = async (payload: {
active_data_file?: DataFileContext | null;
selected_data_source?: string | null;
selected_knowledge_base_id?: string | null;
}) => {
try {
await api.put(`/nanobot/sessions/${encodeURIComponent(activeSessionKey)}/context-file`, payload);
@@ -472,6 +529,16 @@ export function ChatInterface() {
}
};
const handleSelectKnowledgeBase = async (knowledgeBaseId: string) => {
setSelectedKnowledgeBaseId(knowledgeBaseId);
await syncSessionContext({ selected_knowledge_base_id: knowledgeBaseId });
};
const handleClearKnowledgeBase = async () => {
setSelectedKnowledgeBaseId("");
await syncSessionContext({ selected_knowledge_base_id: null });
};
const handleSelectDataSource = async (sourceId: string) => {
setSelectedDataSource(sourceId);
await syncSessionContext({ selected_data_source: sourceId });
@@ -516,6 +583,7 @@ export function ChatInterface() {
reasoningContent: typeof m.reasoning_content === "string" ? m.reasoning_content : undefined,
usage: m.usage,
artifacts: normalizeArtifacts(m.artifacts),
kbCitations: normalizeKnowledgeCitations(m.kb_citations),
};
});
setMessagesForSession(activeSessionKey, formattedMessages);
@@ -524,14 +592,17 @@ export function ChatInterface() {
}
const restoredFile = data.metadata?.active_data_file || null;
const restoredSource = data.metadata?.selected_data_source || "";
const restoredKnowledgeBaseId = data.metadata?.selected_knowledge_base_id || "";
setActiveDataFile(restoredFile);
setSelectedDataSource(restoredSource);
setSelectedKnowledgeBaseId(restoredKnowledgeBaseId);
setAttachedFile(null);
} catch (e) {
console.error("Failed to fetch session messages", e);
setMessagesForSession(activeSessionKey, []);
setActiveDataFile(null);
setSelectedDataSource("");
setSelectedKnowledgeBaseId("");
setAttachedFile(null);
} finally {
setIsLoadingForSession(activeSessionKey, false);
@@ -631,6 +702,7 @@ export function ChatInterface() {
};
const selectedDataSourceName = availableDataSources.find(ds => ds.id === selectedDataSource)?.name || "";
const selectedKnowledgeBaseName = availableKnowledgeBases.find((item) => item.id === selectedKnowledgeBaseId)?.name || "";
const selectedSkills = availableSkills.filter(skill => selectedSkillIds.includes(skill.id));
const isThinkingCollapsed = (messageId: string) => collapsedThinkingByMessage[messageId] ?? true;
const toggleThinkingCollapsed = (messageId: string) => {
@@ -650,7 +722,7 @@ export function ChatInterface() {
};
const renderActiveSelections = () => {
if (!selectedDataSource && selectedSkills.length === 0) return null;
if (!selectedDataSource && !selectedKnowledgeBaseId && selectedSkills.length === 0) return null;
return (
<div className="px-2 pt-2">
<div className="flex flex-wrap gap-2">
@@ -660,6 +732,12 @@ export function ChatInterface() {
{`${t('dataSource')}${selectedDataSourceName}`}
</div>
) : null}
{selectedKnowledgeBaseId ? (
<div className="px-3 py-1.5 rounded-full text-xs border flex items-center gap-1.5 bg-violet-50 text-violet-700 border-violet-200">
<Database className="h-3.5 w-3.5" />
{`${t('knowledgeBase')}${selectedKnowledgeBaseName || selectedKnowledgeBaseId}`}
</div>
) : null}
{selectedSkills.map((skill) => (
<div
key={skill.id}
@@ -812,6 +890,7 @@ export function ChatInterface() {
prefer_sql_chart: preferSqlChart,
file_url: fileUrl,
route_mode: "auto",
knowledge_base_id: selectedKnowledgeBaseId || undefined,
}),
signal: controller.signal,
});
@@ -917,6 +996,7 @@ export function ChatInterface() {
completion_tokens: number;
total_tokens: number;
};
kb_citations?: unknown;
};
if (payload.type === "delta" && payload.content) {
@@ -963,9 +1043,10 @@ export function ChatInterface() {
flushAssistant(true);
pushProgressLog(t('answerGenerationCompleted'));
const messageArtifacts = normalizeArtifacts(payload.artifacts);
const messageCitations = normalizeKnowledgeCitations(payload.kb_citations);
setMessagesForSession(targetSessionKey, (prev) =>
prev.map((msg) =>
msg.id === assistantId ? { ...msg, content: typeof payload.content === "string" ? payload.content : msg.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz, usage: payload.usage, artifacts: messageArtifacts.length > 0 ? messageArtifacts : msg.artifacts } : msg
msg.id === assistantId ? { ...msg, content: typeof payload.content === "string" ? payload.content : msg.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz, usage: payload.usage, artifacts: messageArtifacts.length > 0 ? messageArtifacts : msg.artifacts, kbCitations: messageCitations.length > 0 ? messageCitations : msg.kbCitations } : msg
)
);
}
@@ -1146,6 +1227,52 @@ export function ChatInterface() {
</div>
)}
</div>
<div className="mt-3 pt-3 border-t border-border">
<div className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2 px-2 flex items-center gap-1.5">
<Database className="h-3 w-3" />
{t('knowledgeBase')}
</div>
<div className="space-y-0.5">
{availableKnowledgeBases.length > 0 ? (
availableKnowledgeBases.map((kb) => (
<button
key={kb.id}
onClick={() => {
void handleSelectKnowledgeBase(kb.id);
}}
className={cn(
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm transition-all duration-200",
selectedKnowledgeBaseId === kb.id
? "bg-background text-foreground shadow-sm ring-1 ring-border"
: "text-muted-foreground hover:bg-background hover:shadow-sm"
)}
>
<div className="flex items-center gap-2.5">
<Database className={cn("h-4 w-4", selectedKnowledgeBaseId === kb.id ? "text-violet-500" : "text-muted-foreground")} />
<span className="font-medium">{kb.name}</span>
</div>
{selectedKnowledgeBaseId === kb.id && <CheckCircle2 className="h-4 w-4 text-violet-500" />}
</button>
))
) : (
<div className="px-3 py-3 text-xs text-muted-foreground">
{t('noKnowledgeBases')}
</div>
)}
{selectedKnowledgeBaseId ? (
<div className="mt-2 pt-2 border-t border-border">
<button
onClick={() => {
void handleClearKnowledgeBase();
}}
className="w-full py-1.5 text-[11px] text-muted-foreground hover:text-muted-foreground transition-colors flex items-center justify-center gap-1"
>
{t('clearSelected')}
</button>
</div>
) : null}
</div>
</div>
</div>
{/* Right Column: Skills */}
@@ -1501,6 +1628,22 @@ export function ChatInterface() {
))}
</div>
) : null}
{msg.kbCitations && msg.kbCitations.length > 0 ? (
<div className="mt-4 rounded-xl border border-violet-200 bg-violet-50/60 p-3">
<div className="text-xs font-semibold text-violet-700 uppercase tracking-wider mb-2">{t('knowledgeCitations')}</div>
<div className="space-y-2">
{msg.kbCitations.map((citation, citationIndex) => (
<div key={`${msg.id}-citation-${citationIndex}`} className="rounded-lg border border-violet-200 bg-white/80 px-3 py-2">
<div className="flex items-center justify-between gap-2">
<div className="text-sm font-medium text-violet-900 truncate">{citation.title}</div>
<div className="text-[11px] text-violet-700 shrink-0">{t('matchScore', { score: citation.score.toFixed(3) })}</div>
</div>
<div className="mt-1 text-xs text-violet-800 line-clamp-3 whitespace-pre-wrap break-words">{citation.chunk}</div>
</div>
))}
</div>
</div>
) : null}
{msg.viz ? (
<div className="mt-3 pt-3 border-t border-border">
<InlineVisualizationCard viz={msg.viz} />
@@ -1581,6 +1724,52 @@ export function ChatInterface() {
</div>
)}
</div>
<div className="mt-3 pt-3 border-t border-border">
<div className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2 px-2 flex items-center gap-1.5">
<Database className="h-3 w-3" />
{t('knowledgeBase')}
</div>
<div className="space-y-0.5">
{availableKnowledgeBases.length > 0 ? (
availableKnowledgeBases.map((kb) => (
<button
key={kb.id}
onClick={() => {
void handleSelectKnowledgeBase(kb.id);
}}
className={cn(
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm transition-all duration-200",
selectedKnowledgeBaseId === kb.id
? "bg-background text-foreground shadow-sm ring-1 ring-border"
: "text-muted-foreground hover:bg-background hover:shadow-sm"
)}
>
<div className="flex items-center gap-2.5">
<Database className={cn("h-4 w-4", selectedKnowledgeBaseId === kb.id ? "text-violet-500" : "text-muted-foreground")} />
<span className="font-medium">{kb.name}</span>
</div>
{selectedKnowledgeBaseId === kb.id && <CheckCircle2 className="h-4 w-4 text-violet-500" />}
</button>
))
) : (
<div className="px-3 py-3 text-xs text-muted-foreground">
{t('noKnowledgeBases')}
</div>
)}
{selectedKnowledgeBaseId ? (
<div className="mt-2 pt-2 border-t border-border">
<button
onClick={() => {
void handleClearKnowledgeBase();
}}
className="w-full py-1.5 text-[11px] text-muted-foreground hover:text-muted-foreground transition-colors flex items-center justify-center gap-1"
>
{t('clearSelected')}
</button>
</div>
) : null}
</div>
</div>
</div>
{/* Right Column: Skills */}
+98
View File
@@ -139,6 +139,104 @@
"leaveBlankIfNotModifying": "Leave blank if not modifying",
"confirmNewPassword": "Confirm New Password",
"saveSettings": "Save Settings",
"knowledgeBase": "Knowledge Base",
"knowledgeBaseSettings": "Knowledge Base Configuration",
"knowledgeBaseSettingsDesc": "Create, edit, reindex, and manage knowledge bases in the current project.",
"knowledgeGlobalConfigTitle": "Knowledge Global Configuration",
"knowledgeGlobalConfigDesc": "Configure global API base and key for knowledge service shared across projects.",
"knowledgeGlobalApiBase": "API Base",
"knowledgeGlobalApiBasePlaceholder": "e.g. https://api.siliconflow.cn/v1 (without /embeddings)",
"knowledgeGlobalApiKey": "API Key",
"knowledgeGlobalApiKeyPlaceholder": "Leave blank to keep the current key",
"knowledgeGlobalApiKeyMasked": "Saved key: {{masked}}",
"knowledgeGlobalApiKeyEmpty": "No API key configured",
"knowledgeGlobalDefaultEmbeddingModel": "Default Embedding Model",
"knowledgeGlobalDefaultEmbeddingModelPlaceholder": "e.g. text-embedding-3-small",
"knowledgeGlobalModelNameHint": "API Base should be the provider base URL (without /embeddings), and model name must be explicit for testing and indexing.",
"knowledgeGlobalModelNameTooLong": "Default embedding model name cannot exceed 200 characters",
"knowledgeGlobalConfigLoadFailed": "Failed to load knowledge global configuration",
"knowledgeGlobalConfigSaveFailed": "Failed to save knowledge global configuration",
"knowledgeGlobalConfigSaved": "Knowledge global configuration saved successfully",
"knowledgeGlobalConfigApiBaseInvalid": "API Base must start with http:// or https://",
"knowledgeGlobalConfigApiBaseShouldBeBaseUrl": "API Base must be a base URL and should not include /embeddings",
"testKnowledgeGlobalConnection": "Test Connection",
"knowledgeGlobalConnectionTestPassed": "Connection test passed",
"knowledgeGlobalConnectionTestFailed": "Connection test failed",
"knowledgeGlobalConnectionModelResult": "Model: {{model}}",
"knowledgeGlobalConnectionDimensionResult": "Embedding dimension: {{dim}}",
"knowledgeGlobalConnectionAvailableModelsResult": "Available model examples: {{models}}",
"knowledgeGlobalModelNameRequiredForTest": "Model name is required for connection testing",
"knowledgeGlobalArkModelRequiredForTest": "For Volcengine Ark, model name is required for connection testing (Model ID or Endpoint ID)",
"saveKnowledgeGlobalConfig": "Save Global Configuration",
"refresh": "Refresh",
"knowledgeBaseName": "Knowledge Base Name",
"knowledgeBaseNamePlaceholder": "Enter knowledge base name",
"knowledgeBaseDescriptionPlaceholder": "Enter knowledge base description (optional)",
"knowledgeBaseEmbeddingModel": "Embedding Model",
"knowledgeBaseEmbeddingModelPlaceholder": "e.g. text-embedding-3-large",
"knowledgeBaseChunkSize": "Chunk Size",
"knowledgeBaseChunkOverlap": "Chunk Overlap",
"knowledgeBaseTopK": "Top K",
"createKnowledgeBase": "Create Knowledge Base",
"updateKnowledgeBase": "Update Knowledge Base",
"knowledgeBaseList": "Knowledge Base List",
"knowledgeBaseMeta": "{{count}} docs · Updated {{updatedAt}}",
"manageKnowledgeDocuments": "Manage Documents",
"knowledgeDocumentManagerTitle": "Document Management ({{name}})",
"knowledgeDocumentManagerTitleEmpty": "Knowledge Document Management",
"selectKnowledgeBaseToManageDocuments": "Select a knowledge base above to manage documents",
"knowledgeDocumentTitle": "Document Title",
"knowledgeDocumentTitlePlaceholder": "e.g. Refund Policy",
"knowledgeDocumentContent": "Document Content",
"knowledgeDocumentContentPlaceholder": "Enter document content",
"knowledgeDocumentMetadata": "Document Metadata (Optional JSON)",
"knowledgeDocumentMetadataPlaceholder": "e.g. {\"source\":\"manual\",\"lang\":\"en\"}",
"knowledgeDocumentMeta": "Updated {{updatedAt}}",
"knowledgeDocumentTitleRequired": "Please enter a document title",
"knowledgeDocumentContentRequired": "Please enter document content",
"knowledgeDocumentMetadataInvalid": "Document metadata must be valid JSON",
"createKnowledgeDocument": "Create Document",
"updateKnowledgeDocument": "Update Document",
"editKnowledgeDocument": "Edit Document",
"deleteKnowledgeDocument": "Delete Document",
"confirmDeleteKnowledgeDocument": "Are you sure you want to delete this document?",
"knowledgeDocumentCreated": "Document created successfully",
"knowledgeDocumentUpdated": "Document updated successfully",
"knowledgeDocumentDeleted": "Document deleted successfully",
"knowledgeDocumentLoadFailed": "Failed to load documents",
"knowledgeDocumentSaveFailed": "Failed to save document",
"knowledgeDocumentDeleteFailed": "Failed to delete document",
"noKnowledgeDocuments": "No documents in this knowledge base",
"knowledgeDocumentUploadTitle": "Upload Documents to Knowledge Base",
"knowledgeDocumentUploadHint": "Supports txt, md, json, yaml, xml, html, csv, xls, xlsx. Max 5MB per file.",
"knowledgeDocumentUploadSelected": "{{count}} file(s) selected",
"knowledgeDocumentUploadNone": "No files selected",
"knowledgeDocumentUploadAction": "Upload and Add",
"knowledgeDocumentUploadEmpty": "Please select files to upload",
"knowledgeDocumentUploadSuccess": "{{count}} file(s) uploaded successfully",
"knowledgeDocumentUploadFailed": "Failed to upload documents",
"knowledgeCitations": "Knowledge Citations",
"matchScore": "Score: {{score}}",
"editKnowledgeBase": "Edit Knowledge Base",
"deleteKnowledgeBase": "Delete Knowledge Base",
"reindexKnowledgeBase": "Reindex",
"refreshKnowledgeBaseList": "Refresh Knowledge Bases",
"knowledgeBaseLoadFailed": "Failed to load knowledge bases",
"knowledgeBaseNameRequired": "Please enter a knowledge base name",
"knowledgeBaseChunkSizeRange": "Chunk Size must be between 64 and 4096",
"knowledgeBaseChunkOverlapRange": "Chunk Overlap must be between 0 and 512",
"knowledgeBaseChunkOverlapTooLarge": "Chunk Overlap must be smaller than Chunk Size",
"knowledgeBaseTopKRange": "Top K must be between 1 and 20",
"knowledgeBaseCreated": "Knowledge base created successfully",
"knowledgeBaseUpdated": "Knowledge base updated successfully",
"knowledgeBaseSaveFailed": "Failed to save knowledge base",
"confirmDeleteKnowledgeBase": "Are you sure you want to delete this knowledge base?",
"knowledgeBaseDeleted": "Knowledge base deleted successfully",
"knowledgeBaseDeleteFailed": "Failed to delete knowledge base",
"knowledgeBaseReindexSuccess": "Knowledge base reindexed successfully",
"knowledgeBaseReindexFailed": "Failed to reindex knowledge base",
"selectProjectBeforeManageKnowledgeBase": "Please select a project before managing knowledge bases",
"noKnowledgeBases": "No knowledge bases in this project. Create one in Settings first.",
"confirmDeleteUser": "Are you sure you want to delete this user?",
"newUserMustHavePassword": "New users must have a password",
"anErrorOccurred": "An error occurred",
+98
View File
@@ -152,6 +152,104 @@
"leaveBlankIfNotModifying": "如不修改请留空",
"confirmNewPassword": "确认新密码",
"saveSettings": "保存设置",
"knowledgeBase": "知识库",
"knowledgeBaseSettings": "知识库配置与建库管理",
"knowledgeBaseSettingsDesc": "在当前项目下创建、编辑、重建索引并维护知识库配置。",
"knowledgeGlobalConfigTitle": "知识库全局配置",
"knowledgeGlobalConfigDesc": "配置知识库服务的全局 API 地址与密钥,所有项目共享。",
"knowledgeGlobalApiBase": "API Base",
"knowledgeGlobalApiBasePlaceholder": "例如:https://api.siliconflow.cn/v1(不要填写 /embeddings",
"knowledgeGlobalApiKey": "API Key",
"knowledgeGlobalApiKeyPlaceholder": "留空表示保持当前密钥不变",
"knowledgeGlobalApiKeyMasked": "当前已保存密钥:{{masked}}",
"knowledgeGlobalApiKeyEmpty": "当前未配置 API Key",
"knowledgeGlobalDefaultEmbeddingModel": "默认向量模型名称",
"knowledgeGlobalDefaultEmbeddingModelPlaceholder": "例如:text-embedding-3-small",
"knowledgeGlobalModelNameHint": "API Base 请填写模型服务基地址(不含 /embeddings),模型名称需显式填写用于测试与建库。",
"knowledgeGlobalModelNameTooLong": "默认向量模型名称长度不能超过 200 个字符",
"knowledgeGlobalConfigLoadFailed": "加载知识库全局配置失败",
"knowledgeGlobalConfigSaveFailed": "保存知识库全局配置失败",
"knowledgeGlobalConfigSaved": "知识库全局配置保存成功",
"knowledgeGlobalConfigApiBaseInvalid": "API Base 需以 http:// 或 https:// 开头",
"knowledgeGlobalConfigApiBaseShouldBeBaseUrl": "API Base 需填写基地址,不要包含 /embeddings",
"testKnowledgeGlobalConnection": "测试连接",
"knowledgeGlobalConnectionTestPassed": "测试连接成功",
"knowledgeGlobalConnectionTestFailed": "测试连接失败",
"knowledgeGlobalConnectionModelResult": "模型:{{model}}",
"knowledgeGlobalConnectionDimensionResult": "向量维度:{{dim}}",
"knowledgeGlobalConnectionAvailableModelsResult": "可用模型示例:{{models}}",
"knowledgeGlobalModelNameRequiredForTest": "测试连接必须填写向量模型名称",
"knowledgeGlobalArkModelRequiredForTest": "火山方舟测试连接需填写向量模型名称(Model ID 或 Endpoint ID",
"saveKnowledgeGlobalConfig": "保存全局配置",
"refresh": "刷新",
"knowledgeBaseName": "知识库名称",
"knowledgeBaseNamePlaceholder": "请输入知识库名称",
"knowledgeBaseDescriptionPlaceholder": "请输入知识库描述(可选)",
"knowledgeBaseEmbeddingModel": "Embedding 模型",
"knowledgeBaseEmbeddingModelPlaceholder": "例如:text-embedding-3-large",
"knowledgeBaseChunkSize": "Chunk Size",
"knowledgeBaseChunkOverlap": "Chunk Overlap",
"knowledgeBaseTopK": "Top K",
"createKnowledgeBase": "创建知识库",
"updateKnowledgeBase": "更新知识库",
"knowledgeBaseList": "知识库列表",
"knowledgeBaseMeta": "文档 {{count}} 个 · 更新时间 {{updatedAt}}",
"manageKnowledgeDocuments": "管理文档",
"knowledgeDocumentManagerTitle": "文档管理({{name}}",
"knowledgeDocumentManagerTitleEmpty": "知识库文档管理",
"selectKnowledgeBaseToManageDocuments": "请先从上方知识库列表选择一个知识库后再管理文档",
"knowledgeDocumentTitle": "文档标题",
"knowledgeDocumentTitlePlaceholder": "例如:退款政策说明",
"knowledgeDocumentContent": "文档内容",
"knowledgeDocumentContentPlaceholder": "请输入文档正文内容",
"knowledgeDocumentMetadata": "文档元数据(JSON,可选)",
"knowledgeDocumentMetadataPlaceholder": "例如:{\"source\":\"manual\",\"lang\":\"zh\"}",
"knowledgeDocumentMeta": "更新于 {{updatedAt}}",
"knowledgeDocumentTitleRequired": "请输入文档标题",
"knowledgeDocumentContentRequired": "请输入文档内容",
"knowledgeDocumentMetadataInvalid": "文档元数据必须是合法的 JSON",
"createKnowledgeDocument": "新增文档",
"updateKnowledgeDocument": "更新文档",
"editKnowledgeDocument": "编辑文档",
"deleteKnowledgeDocument": "删除文档",
"confirmDeleteKnowledgeDocument": "确定删除该文档吗?",
"knowledgeDocumentCreated": "文档创建成功",
"knowledgeDocumentUpdated": "文档更新成功",
"knowledgeDocumentDeleted": "文档删除成功",
"knowledgeDocumentLoadFailed": "加载文档失败",
"knowledgeDocumentSaveFailed": "保存文档失败",
"knowledgeDocumentDeleteFailed": "删除文档失败",
"noKnowledgeDocuments": "当前知识库还没有文档",
"knowledgeDocumentUploadTitle": "上传文档到知识库",
"knowledgeDocumentUploadHint": "支持 txt、md、json、yaml、xml、html、csv、xls、xlsx,单文件不超过 5MB。",
"knowledgeDocumentUploadSelected": "已选择 {{count}} 个文件",
"knowledgeDocumentUploadNone": "尚未选择文件",
"knowledgeDocumentUploadAction": "上传并入库",
"knowledgeDocumentUploadEmpty": "请先选择要上传的文件",
"knowledgeDocumentUploadSuccess": "已成功上传 {{count}} 个文件",
"knowledgeDocumentUploadFailed": "上传文档失败",
"knowledgeCitations": "知识库引用片段",
"matchScore": "匹配分:{{score}}",
"editKnowledgeBase": "编辑知识库",
"deleteKnowledgeBase": "删除知识库",
"reindexKnowledgeBase": "重建索引",
"refreshKnowledgeBaseList": "刷新知识库列表",
"knowledgeBaseLoadFailed": "加载知识库失败",
"knowledgeBaseNameRequired": "请输入知识库名称",
"knowledgeBaseChunkSizeRange": "Chunk Size 需在 64 到 4096 之间",
"knowledgeBaseChunkOverlapRange": "Chunk Overlap 需在 0 到 512 之间",
"knowledgeBaseChunkOverlapTooLarge": "Chunk Overlap 需小于 Chunk Size",
"knowledgeBaseTopKRange": "Top K 需在 1 到 20 之间",
"knowledgeBaseCreated": "知识库创建成功",
"knowledgeBaseUpdated": "知识库更新成功",
"knowledgeBaseSaveFailed": "保存知识库失败",
"confirmDeleteKnowledgeBase": "确定要删除这个知识库吗?",
"knowledgeBaseDeleted": "知识库删除成功",
"knowledgeBaseDeleteFailed": "删除知识库失败",
"knowledgeBaseReindexSuccess": "知识库重建索引成功",
"knowledgeBaseReindexFailed": "知识库重建索引失败",
"selectProjectBeforeManageKnowledgeBase": "请先选择一个项目,再进行知识库管理",
"noKnowledgeBases": "当前项目暂无知识库,请先在设置中创建",
"confirmDeleteUser": "确认删除该用户吗?",
"newUserMustHavePassword": "新建用户必须填写密码",
"anErrorOccurred": "发生错误",
+923 -4
View File
@@ -4,17 +4,119 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import { Save, Loader2 } from "lucide-react";
import { Save, Loader2, Database, RefreshCw, Pencil, Trash2, FileText, Plus } from "lucide-react";
import { api } from "@/lib/api";
import { useAuthStore } from "@/store/authStore";
import { useProjectStore } from "@/store/projectStore";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
interface KnowledgeBase {
id: string;
name: string;
description?: string;
project_id?: number | null;
embedding_model?: string | null;
chunk_size: number;
chunk_overlap: number;
top_k: number;
is_active: boolean;
updated_at: string;
documents?: Array<{ id: string }>;
}
interface KnowledgeBaseForm {
name: string;
description: string;
embedding_model: string;
chunk_size: number;
chunk_overlap: number;
top_k: number;
is_active: boolean;
}
interface KnowledgeDocument {
id: string;
title: string;
content: string;
metadata?: Record<string, unknown>;
created_at: string;
updated_at: string;
}
interface KnowledgeGlobalConfig {
api_base?: string | null;
api_key?: string | null;
api_key_masked?: string | null;
has_api_key: boolean;
default_embedding_model?: string | null;
}
interface KnowledgeConnectionTestResult {
success: boolean;
message: string;
model_name?: string | null;
embedding_dimension?: number | null;
resolved_api_base?: string | null;
available_models?: string[];
}
const defaultKnowledgeBaseForm: KnowledgeBaseForm = {
name: '',
description: '',
embedding_model: '',
chunk_size: 512,
chunk_overlap: 50,
top_k: 3,
is_active: true,
};
const defaultKnowledgeDocumentForm = {
title: '',
content: '',
metadata: '',
};
export function Settings() {
const { t } = useTranslation();
const { user, updateUser } = useAuthStore();
const { currentProject } = useProjectStore();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [isLoadingKnowledgeBases, setIsLoadingKnowledgeBases] = useState(false);
const [isSavingKnowledgeBase, setIsSavingKnowledgeBase] = useState(false);
const [deletingKnowledgeBaseId, setDeletingKnowledgeBaseId] = useState('');
const [reindexingKnowledgeBaseId, setReindexingKnowledgeBaseId] = useState('');
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
const [editingKnowledgeBaseId, setEditingKnowledgeBaseId] = useState('');
const [knowledgeBaseForm, setKnowledgeBaseForm] = useState<KnowledgeBaseForm>(defaultKnowledgeBaseForm);
const [knowledgeGlobalConfig, setKnowledgeGlobalConfig] = useState<KnowledgeGlobalConfig>({
api_base: '',
api_key: null,
api_key_masked: null,
has_api_key: false,
default_embedding_model: '',
});
const [knowledgeGlobalForm, setKnowledgeGlobalForm] = useState({
api_base: '',
api_key: '',
default_embedding_model: '',
});
const [isLoadingKnowledgeGlobalConfig, setIsLoadingKnowledgeGlobalConfig] = useState(false);
const [isSavingKnowledgeGlobalConfig, setIsSavingKnowledgeGlobalConfig] = useState(false);
const [isTestingKnowledgeGlobalConnection, setIsTestingKnowledgeGlobalConnection] = useState(false);
const [knowledgeConnectionTestResult, setKnowledgeConnectionTestResult] = useState<KnowledgeConnectionTestResult | null>(null);
const [selectedKnowledgeBaseId, setSelectedKnowledgeBaseId] = useState('');
const [knowledgeDocuments, setKnowledgeDocuments] = useState<KnowledgeDocument[]>([]);
const [isLoadingKnowledgeDocuments, setIsLoadingKnowledgeDocuments] = useState(false);
const [isSavingKnowledgeDocument, setIsSavingKnowledgeDocument] = useState(false);
const [deletingKnowledgeDocumentId, setDeletingKnowledgeDocumentId] = useState('');
const [editingKnowledgeDocumentId, setEditingKnowledgeDocumentId] = useState('');
const [knowledgeDocumentForm, setKnowledgeDocumentForm] = useState(defaultKnowledgeDocumentForm);
const [uploadingKnowledgeDocuments, setUploadingKnowledgeDocuments] = useState(false);
const [knowledgeUploadFiles, setKnowledgeUploadFiles] = useState<File[]>([]);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
@@ -23,8 +125,430 @@ export function Settings() {
setEmail(user.email || '');
}
}, [user]);
useEffect(() => {
void fetchKnowledgeBases();
}, [currentProject?.id]);
useEffect(() => {
void fetchKnowledgeGlobalConfig();
}, []);
const isPasswordMismatch = password !== '' && confirmPassword !== '' && password !== confirmPassword;
const fetchKnowledgeGlobalConfig = async () => {
setIsLoadingKnowledgeGlobalConfig(true);
try {
const data = await api.get<KnowledgeGlobalConfig>('/api/v1/knowledge-bases/global-config');
setKnowledgeGlobalConfig(data);
setKnowledgeGlobalForm({
api_base: data.api_base || '',
api_key: '',
default_embedding_model: data.default_embedding_model || '',
});
} catch (err: any) {
setError(err.message || t('knowledgeGlobalConfigLoadFailed'));
} finally {
setIsLoadingKnowledgeGlobalConfig(false);
}
};
const validateKnowledgeGlobalConfig = () => {
const normalizedApiBase = knowledgeGlobalForm.api_base.trim();
if (!normalizedApiBase) {
return '';
}
if (!(normalizedApiBase.startsWith('http://') || normalizedApiBase.startsWith('https://'))) {
return t('knowledgeGlobalConfigApiBaseInvalid');
}
if (normalizedApiBase.toLowerCase().endsWith('/embeddings')) {
return t('knowledgeGlobalConfigApiBaseShouldBeBaseUrl');
}
return '';
};
const validateKnowledgeGlobalModelName = () => {
const normalizedModelName = knowledgeGlobalForm.default_embedding_model.trim();
if (!normalizedModelName) {
return '';
}
if (normalizedModelName.length > 200) {
return t('knowledgeGlobalModelNameTooLong');
}
return '';
};
const handleSaveKnowledgeGlobalConfig = async () => {
setError('');
setSuccess('');
const validationMessage = validateKnowledgeGlobalConfig();
if (validationMessage) {
setError(validationMessage);
return;
}
const modelValidationMessage = validateKnowledgeGlobalModelName();
if (modelValidationMessage) {
setError(modelValidationMessage);
return;
}
setIsSavingKnowledgeGlobalConfig(true);
try {
const payload: Record<string, string | null> = {
api_base: knowledgeGlobalForm.api_base.trim() || null,
default_embedding_model: knowledgeGlobalForm.default_embedding_model.trim() || null,
};
const normalizedApiKey = knowledgeGlobalForm.api_key.trim();
if (normalizedApiKey) {
payload.api_key = normalizedApiKey;
}
const data = await api.put<KnowledgeGlobalConfig>('/api/v1/knowledge-bases/global-config', payload);
setKnowledgeGlobalConfig(data);
setKnowledgeGlobalForm({
api_base: data.api_base || '',
api_key: '',
default_embedding_model: data.default_embedding_model || '',
});
setKnowledgeConnectionTestResult(null);
setSuccess(t('knowledgeGlobalConfigSaved'));
} catch (err: any) {
setError(err.message || t('knowledgeGlobalConfigSaveFailed'));
} finally {
setIsSavingKnowledgeGlobalConfig(false);
}
};
const handleTestKnowledgeGlobalConnection = async () => {
setError('');
setSuccess('');
setKnowledgeConnectionTestResult(null);
const validationMessage = validateKnowledgeGlobalConfig();
if (validationMessage) {
setError(validationMessage);
return;
}
const modelValidationMessage = validateKnowledgeGlobalModelName();
if (modelValidationMessage) {
setError(modelValidationMessage);
return;
}
const normalizedModelName = knowledgeGlobalForm.default_embedding_model.trim();
if (!normalizedModelName) {
setError(t('knowledgeGlobalModelNameRequiredForTest'));
return;
}
setIsTestingKnowledgeGlobalConnection(true);
try {
const payload: Record<string, string> = {};
const normalizedApiBase = knowledgeGlobalForm.api_base.trim();
const normalizedApiKey = knowledgeGlobalForm.api_key.trim();
if (normalizedApiBase) payload.api_base = normalizedApiBase;
if (normalizedApiKey) payload.api_key = normalizedApiKey;
if (normalizedModelName) payload.model_name = normalizedModelName;
const result = await api.post<KnowledgeConnectionTestResult>('/api/v1/knowledge-bases/global-config/test-connection', payload);
setKnowledgeConnectionTestResult(result);
setSuccess(t('knowledgeGlobalConnectionTestPassed'));
} catch (err: any) {
setError(err.message || t('knowledgeGlobalConnectionTestFailed'));
} finally {
setIsTestingKnowledgeGlobalConnection(false);
}
};
const fetchKnowledgeBases = async () => {
if (!currentProject) {
setKnowledgeBases([]);
return;
}
setIsLoadingKnowledgeBases(true);
try {
const data = await api.get<KnowledgeBase[]>(`/api/v1/knowledge-bases?project_id=${currentProject.id}`);
setKnowledgeBases(data);
if (editingKnowledgeBaseId && !data.find((item) => item.id === editingKnowledgeBaseId)) {
setEditingKnowledgeBaseId('');
setKnowledgeBaseForm(defaultKnowledgeBaseForm);
}
if (selectedKnowledgeBaseId && !data.find((item) => item.id === selectedKnowledgeBaseId)) {
setSelectedKnowledgeBaseId('');
setKnowledgeDocuments([]);
setEditingKnowledgeDocumentId('');
setKnowledgeDocumentForm(defaultKnowledgeDocumentForm);
}
} catch (err: any) {
setError(err.message || t('knowledgeBaseLoadFailed'));
} finally {
setIsLoadingKnowledgeBases(false);
}
};
const resetKnowledgeBaseForm = () => {
setEditingKnowledgeBaseId('');
setKnowledgeBaseForm(defaultKnowledgeBaseForm);
};
const validateKnowledgeBaseForm = () => {
if (!currentProject) {
return t('selectProjectBeforeManageKnowledgeBase');
}
if (!knowledgeBaseForm.name.trim()) {
return t('knowledgeBaseNameRequired');
}
if (knowledgeBaseForm.chunk_size < 64 || knowledgeBaseForm.chunk_size > 4096) {
return t('knowledgeBaseChunkSizeRange');
}
if (knowledgeBaseForm.chunk_overlap < 0 || knowledgeBaseForm.chunk_overlap > 512) {
return t('knowledgeBaseChunkOverlapRange');
}
if (knowledgeBaseForm.chunk_overlap >= knowledgeBaseForm.chunk_size) {
return t('knowledgeBaseChunkOverlapTooLarge');
}
if (knowledgeBaseForm.top_k < 1 || knowledgeBaseForm.top_k > 20) {
return t('knowledgeBaseTopKRange');
}
return '';
};
const handleSaveKnowledgeBase = async () => {
setError('');
setSuccess('');
const validationMessage = validateKnowledgeBaseForm();
if (validationMessage) {
setError(validationMessage);
return;
}
if (!currentProject) return;
setIsSavingKnowledgeBase(true);
try {
const payload = {
name: knowledgeBaseForm.name.trim(),
description: knowledgeBaseForm.description.trim() || null,
embedding_model: knowledgeBaseForm.embedding_model.trim() || null,
chunk_size: knowledgeBaseForm.chunk_size,
chunk_overlap: knowledgeBaseForm.chunk_overlap,
top_k: knowledgeBaseForm.top_k,
is_active: knowledgeBaseForm.is_active,
project_id: currentProject.id,
};
if (editingKnowledgeBaseId) {
await api.put(`/api/v1/knowledge-bases/${editingKnowledgeBaseId}`, payload);
setSuccess(t('knowledgeBaseUpdated'));
} else {
await api.post('/api/v1/knowledge-bases', payload);
setSuccess(t('knowledgeBaseCreated'));
}
await fetchKnowledgeBases();
resetKnowledgeBaseForm();
} catch (err: any) {
setError(err.message || t('knowledgeBaseSaveFailed'));
} finally {
setIsSavingKnowledgeBase(false);
}
};
const handleEditKnowledgeBase = (item: KnowledgeBase) => {
setEditingKnowledgeBaseId(item.id);
setKnowledgeBaseForm({
name: item.name || '',
description: item.description || '',
embedding_model: item.embedding_model || '',
chunk_size: item.chunk_size,
chunk_overlap: item.chunk_overlap,
top_k: item.top_k,
is_active: item.is_active,
});
};
const handleDeleteKnowledgeBase = async (id: string) => {
if (!window.confirm(t('confirmDeleteKnowledgeBase'))) {
return;
}
setError('');
setSuccess('');
setDeletingKnowledgeBaseId(id);
try {
await api.delete(`/api/v1/knowledge-bases/${id}`);
setSuccess(t('knowledgeBaseDeleted'));
if (editingKnowledgeBaseId === id) {
resetKnowledgeBaseForm();
}
if (selectedKnowledgeBaseId === id) {
setSelectedKnowledgeBaseId('');
setKnowledgeDocuments([]);
setEditingKnowledgeDocumentId('');
setKnowledgeDocumentForm(defaultKnowledgeDocumentForm);
}
await fetchKnowledgeBases();
} catch (err: any) {
setError(err.message || t('knowledgeBaseDeleteFailed'));
} finally {
setDeletingKnowledgeBaseId('');
}
};
const handleReindexKnowledgeBase = async (id: string) => {
setError('');
setSuccess('');
setReindexingKnowledgeBaseId(id);
try {
await api.post(`/api/v1/knowledge-bases/${id}/reindex`, {});
setSuccess(t('knowledgeBaseReindexSuccess'));
} catch (err: any) {
setError(err.message || t('knowledgeBaseReindexFailed'));
} finally {
setReindexingKnowledgeBaseId('');
}
};
const resetKnowledgeDocumentForm = () => {
setEditingKnowledgeDocumentId('');
setKnowledgeDocumentForm(defaultKnowledgeDocumentForm);
};
const fetchKnowledgeDocuments = async (kbId: string) => {
if (!kbId) {
setKnowledgeDocuments([]);
return;
}
setIsLoadingKnowledgeDocuments(true);
try {
const data = await api.get<KnowledgeDocument[]>(`/api/v1/knowledge-bases/${kbId}/documents`);
setKnowledgeDocuments(data);
if (editingKnowledgeDocumentId && !data.find((item) => item.id === editingKnowledgeDocumentId)) {
resetKnowledgeDocumentForm();
}
} catch (err: any) {
setError(err.message || t('knowledgeDocumentLoadFailed'));
} finally {
setIsLoadingKnowledgeDocuments(false);
}
};
const handleOpenKnowledgeDocuments = async (kbId: string) => {
if (selectedKnowledgeBaseId === kbId) {
setSelectedKnowledgeBaseId('');
setKnowledgeDocuments([]);
resetKnowledgeDocumentForm();
return;
}
setSelectedKnowledgeBaseId(kbId);
resetKnowledgeDocumentForm();
await fetchKnowledgeDocuments(kbId);
};
const validateKnowledgeDocumentForm = () => {
if (!selectedKnowledgeBaseId) {
return t('selectKnowledgeBaseToManageDocuments');
}
if (!knowledgeDocumentForm.title.trim()) {
return t('knowledgeDocumentTitleRequired');
}
if (!knowledgeDocumentForm.content.trim()) {
return t('knowledgeDocumentContentRequired');
}
const metadataText = knowledgeDocumentForm.metadata.trim();
if (!metadataText) {
return '';
}
try {
JSON.parse(metadataText);
return '';
} catch {
return t('knowledgeDocumentMetadataInvalid');
}
};
const handleSaveKnowledgeDocument = async () => {
setError('');
setSuccess('');
const validationMessage = validateKnowledgeDocumentForm();
if (validationMessage) {
setError(validationMessage);
return;
}
if (!selectedKnowledgeBaseId) return;
setIsSavingKnowledgeDocument(true);
try {
const metadataText = knowledgeDocumentForm.metadata.trim();
const payload = {
title: knowledgeDocumentForm.title.trim(),
content: knowledgeDocumentForm.content.trim(),
metadata: metadataText ? JSON.parse(metadataText) : {},
};
if (editingKnowledgeDocumentId) {
await api.put(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/${editingKnowledgeDocumentId}`, payload);
setSuccess(t('knowledgeDocumentUpdated'));
} else {
await api.post(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents`, payload);
setSuccess(t('knowledgeDocumentCreated'));
}
await fetchKnowledgeDocuments(selectedKnowledgeBaseId);
await fetchKnowledgeBases();
resetKnowledgeDocumentForm();
} catch (err: any) {
setError(err.message || t('knowledgeDocumentSaveFailed'));
} finally {
setIsSavingKnowledgeDocument(false);
}
};
const handleEditKnowledgeDocument = (item: KnowledgeDocument) => {
setEditingKnowledgeDocumentId(item.id);
setKnowledgeDocumentForm({
title: item.title || '',
content: item.content || '',
metadata: item.metadata && Object.keys(item.metadata).length > 0 ? JSON.stringify(item.metadata, null, 2) : '',
});
};
const handleDeleteKnowledgeDocument = async (docId: string) => {
if (!selectedKnowledgeBaseId) return;
if (!window.confirm(t('confirmDeleteKnowledgeDocument'))) {
return;
}
setError('');
setSuccess('');
setDeletingKnowledgeDocumentId(docId);
try {
await api.delete(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/${docId}`);
if (editingKnowledgeDocumentId === docId) {
resetKnowledgeDocumentForm();
}
setSuccess(t('knowledgeDocumentDeleted'));
await fetchKnowledgeDocuments(selectedKnowledgeBaseId);
await fetchKnowledgeBases();
} catch (err: any) {
setError(err.message || t('knowledgeDocumentDeleteFailed'));
} finally {
setDeletingKnowledgeDocumentId('');
}
};
const handleUploadKnowledgeDocuments = async () => {
setError('');
setSuccess('');
if (!selectedKnowledgeBaseId) {
setError(t('selectKnowledgeBaseToManageDocuments'));
return;
}
if (knowledgeUploadFiles.length === 0) {
setError(t('knowledgeDocumentUploadEmpty'));
return;
}
setUploadingKnowledgeDocuments(true);
try {
const formData = new FormData();
knowledgeUploadFiles.forEach((file) => formData.append('files', file));
await api.post(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/upload`, formData);
setSuccess(t('knowledgeDocumentUploadSuccess', { count: knowledgeUploadFiles.length }));
setKnowledgeUploadFiles([]);
await fetchKnowledgeDocuments(selectedKnowledgeBaseId);
await fetchKnowledgeBases();
} catch (err: any) {
setError(err.message || t('knowledgeDocumentUploadFailed'));
} finally {
setUploadingKnowledgeDocuments(false);
}
};
const handleSave = async () => {
setError('');
setSuccess('');
@@ -65,17 +589,19 @@ export function Settings() {
}
};
const selectedKnowledgeBase = knowledgeBases.find((item) => item.id === selectedKnowledgeBaseId) || null;
return (
<div className="flex-1 flex flex-col h-full bg-muted/50/30 overflow-hidden">
<div className="h-14 px-6 flex items-center justify-between border-b border-border bg-background">
<div className="flex items-center gap-2 text-foreground/80 font-medium">
<Save className="h-5 w-5 text-indigo-500" />
{t('personalSettings')}
</div>
</div>
<div className="flex-1 p-6 overflow-auto">
<div className="grid gap-6 max-w-2xl mx-auto">
<div className="grid gap-6 max-w-4xl mx-auto">
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded-md p-3">{error}</div>}
{success && <div className="text-sm text-emerald-600 bg-emerald-50 border border-emerald-100 rounded-md p-3">{success}</div>}
@@ -138,7 +664,400 @@ export function Settings() {
<CardFooter className="bg-muted/50/50 border-t border-border pt-6">
<Button onClick={handleSave} className="ml-auto bg-indigo-600 hover:bg-indigo-700 text-primary-foreground" disabled={isSaving || isPasswordMismatch}>
{isSaving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
{t('saveSettings')}
</Button>
</CardFooter>
</Card>
<Card className="border-border shadow-sm">
<CardHeader>
<CardTitle className="text-xl flex items-center gap-2">
<Database className="h-5 w-5 text-indigo-500" />
{t('knowledgeBaseSettings')}
</CardTitle>
<CardDescription>{t('knowledgeBaseSettingsDesc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="rounded-lg border border-border p-4 space-y-4">
<div className="space-y-1">
<div className="text-sm font-medium text-foreground">{t('knowledgeGlobalConfigTitle')}</div>
<div className="text-xs text-muted-foreground">{t('knowledgeGlobalConfigDesc')}</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2 md:col-span-2">
<Label htmlFor="knowledge-global-api-base">{t('knowledgeGlobalApiBase')}</Label>
<Input
id="knowledge-global-api-base"
value={knowledgeGlobalForm.api_base}
placeholder={t('knowledgeGlobalApiBasePlaceholder')}
onChange={(e) => setKnowledgeGlobalForm((prev) => ({ ...prev, api_base: e.target.value }))}
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="knowledge-global-api-key">{t('knowledgeGlobalApiKey')}</Label>
<Input
id="knowledge-global-api-key"
type="password"
value={knowledgeGlobalForm.api_key}
placeholder={t('knowledgeGlobalApiKeyPlaceholder')}
onChange={(e) => setKnowledgeGlobalForm((prev) => ({ ...prev, api_key: e.target.value }))}
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig}
/>
<div className="text-xs text-muted-foreground">
{knowledgeGlobalConfig.has_api_key
? t('knowledgeGlobalApiKeyMasked', { masked: knowledgeGlobalConfig.api_key_masked || '******' })
: t('knowledgeGlobalApiKeyEmpty')}
</div>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="knowledge-global-default-embedding-model">{t('knowledgeGlobalDefaultEmbeddingModel')}</Label>
<Input
id="knowledge-global-default-embedding-model"
value={knowledgeGlobalForm.default_embedding_model}
placeholder={t('knowledgeGlobalDefaultEmbeddingModelPlaceholder')}
onChange={(e) => setKnowledgeGlobalForm((prev) => ({ ...prev, default_embedding_model: e.target.value }))}
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig || isTestingKnowledgeGlobalConnection}
/>
<div className="text-xs text-muted-foreground">
{t('knowledgeGlobalModelNameHint')}
</div>
</div>
</div>
{knowledgeConnectionTestResult ? (
<div className="rounded-md border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-700 space-y-1">
<div>{knowledgeConnectionTestResult.message}</div>
{knowledgeConnectionTestResult.model_name ? (
<div>{t('knowledgeGlobalConnectionModelResult', { model: knowledgeConnectionTestResult.model_name })}</div>
) : null}
{typeof knowledgeConnectionTestResult.embedding_dimension === 'number' ? (
<div>{t('knowledgeGlobalConnectionDimensionResult', { dim: knowledgeConnectionTestResult.embedding_dimension })}</div>
) : null}
{knowledgeConnectionTestResult.available_models && knowledgeConnectionTestResult.available_models.length > 0 ? (
<div>{t('knowledgeGlobalConnectionAvailableModelsResult', { models: knowledgeConnectionTestResult.available_models.slice(0, 5).join(', ') })}</div>
) : null}
</div>
) : null}
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
onClick={handleTestKnowledgeGlobalConnection}
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig || isTestingKnowledgeGlobalConnection}
>
{isTestingKnowledgeGlobalConnection ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Database className="h-4 w-4 mr-2" />}
{t('testKnowledgeGlobalConnection')}
</Button>
<Button
variant="outline"
onClick={() => void fetchKnowledgeGlobalConfig()}
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig || isTestingKnowledgeGlobalConnection}
>
{isLoadingKnowledgeGlobalConfig ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-2" />}
{t('refresh')}
</Button>
<Button onClick={handleSaveKnowledgeGlobalConfig} disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig || isTestingKnowledgeGlobalConnection}>
{isSavingKnowledgeGlobalConfig ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
{t('saveKnowledgeGlobalConfig')}
</Button>
</div>
</div>
{!currentProject ? (
<div className="text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-md p-3">
{t('selectProjectBeforeManageKnowledgeBase')}
</div>
) : null}
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2 md:col-span-2">
<Label htmlFor="knowledge-base-name">{t('knowledgeBaseName')}</Label>
<Input
id="knowledge-base-name"
value={knowledgeBaseForm.name}
placeholder={t('knowledgeBaseNamePlaceholder')}
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, name: e.target.value }))}
disabled={!currentProject}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="knowledge-base-description">{t('description')}</Label>
<Input
id="knowledge-base-description"
value={knowledgeBaseForm.description}
placeholder={t('knowledgeBaseDescriptionPlaceholder')}
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, description: e.target.value }))}
disabled={!currentProject}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="knowledge-base-embedding-model">{t('knowledgeBaseEmbeddingModel')}</Label>
<Input
id="knowledge-base-embedding-model"
value={knowledgeBaseForm.embedding_model}
placeholder={t('knowledgeBaseEmbeddingModelPlaceholder')}
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, embedding_model: e.target.value }))}
disabled={!currentProject}
/>
</div>
<div className="space-y-2">
<Label htmlFor="knowledge-base-chunk-size">{t('knowledgeBaseChunkSize')}</Label>
<Input
id="knowledge-base-chunk-size"
type="number"
value={knowledgeBaseForm.chunk_size}
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, chunk_size: Number(e.target.value) || 0 }))}
disabled={!currentProject}
/>
</div>
<div className="space-y-2">
<Label htmlFor="knowledge-base-chunk-overlap">{t('knowledgeBaseChunkOverlap')}</Label>
<Input
id="knowledge-base-chunk-overlap"
type="number"
value={knowledgeBaseForm.chunk_overlap}
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, chunk_overlap: Number(e.target.value) || 0 }))}
disabled={!currentProject}
/>
</div>
<div className="space-y-2">
<Label htmlFor="knowledge-base-top-k">{t('knowledgeBaseTopK')}</Label>
<Input
id="knowledge-base-top-k"
type="number"
value={knowledgeBaseForm.top_k}
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, top_k: Number(e.target.value) || 0 }))}
disabled={!currentProject}
/>
</div>
<div className="flex items-center justify-between rounded-lg border border-border px-3 py-2 mt-7">
<Label htmlFor="knowledge-base-active">{t('activeStatus')}</Label>
<Switch
id="knowledge-base-active"
checked={knowledgeBaseForm.is_active}
onCheckedChange={(checked) => setKnowledgeBaseForm((prev) => ({ ...prev, is_active: checked }))}
disabled={!currentProject}
/>
</div>
</div>
<div className="flex items-center justify-end gap-2">
{editingKnowledgeBaseId ? (
<Button variant="outline" onClick={resetKnowledgeBaseForm} disabled={isSavingKnowledgeBase}>
{t('cancel')}
</Button>
) : null}
<Button onClick={handleSaveKnowledgeBase} disabled={!currentProject || isSavingKnowledgeBase}>
{isSavingKnowledgeBase ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
{editingKnowledgeBaseId ? t('updateKnowledgeBase') : t('createKnowledgeBase')}
</Button>
</div>
<div className="border-t border-border pt-4 space-y-3">
<div className="font-medium text-sm text-foreground/80">{t('knowledgeBaseList')}</div>
{isLoadingKnowledgeBases ? (
<div className="h-20 flex items-center justify-center text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
</div>
) : knowledgeBases.length === 0 ? (
<div className="text-sm text-muted-foreground rounded-md border border-dashed border-border p-4">
{t('noKnowledgeBases')}
</div>
) : (
<div className="space-y-2">
{knowledgeBases.map((item) => (
<div key={item.id} className="rounded-lg border border-border p-3 flex items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<div className="font-medium text-sm text-foreground truncate">{item.name}</div>
<div className="text-xs text-muted-foreground">
{t('knowledgeBaseMeta', {
count: item.documents?.length || 0,
updatedAt: new Date(item.updated_at).toLocaleString(),
})}
</div>
{item.description ? (
<div className="text-xs text-muted-foreground break-words">{item.description}</div>
) : null}
</div>
<div className="flex items-center gap-1 shrink-0">
<Button
variant="ghost"
size="icon"
onClick={() => {
void handleOpenKnowledgeDocuments(item.id);
}}
title={t('manageKnowledgeDocuments')}
>
{selectedKnowledgeBaseId === item.id ? <Plus className="h-4 w-4 text-indigo-500" /> : <FileText className="h-4 w-4" />}
</Button>
<Button variant="ghost" size="icon" onClick={() => handleEditKnowledgeBase(item)} title={t('editKnowledgeBase')}>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
void handleReindexKnowledgeBase(item.id);
}}
disabled={reindexingKnowledgeBaseId === item.id}
title={t('reindexKnowledgeBase')}
>
{reindexingKnowledgeBaseId === item.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
void handleDeleteKnowledgeBase(item.id);
}}
disabled={deletingKnowledgeBaseId === item.id}
title={t('deleteKnowledgeBase')}
>
{deletingKnowledgeBaseId === item.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4 text-red-500" />}
</Button>
</div>
</div>
))}
</div>
)}
</div>
<div className="border-t border-border pt-4 space-y-3">
<div className="font-medium text-sm text-foreground/80">
{selectedKnowledgeBase
? t('knowledgeDocumentManagerTitle', { name: selectedKnowledgeBase.name })
: t('knowledgeDocumentManagerTitleEmpty')}
</div>
{!selectedKnowledgeBase ? (
<div className="text-sm text-muted-foreground rounded-md border border-dashed border-border p-4">
{t('selectKnowledgeBaseToManageDocuments')}
</div>
) : (
<div className="space-y-4">
<div className="rounded-lg border border-border p-3 space-y-3">
<div className="text-sm font-medium text-foreground">{t('knowledgeDocumentUploadTitle')}</div>
<Input
type="file"
multiple
onChange={(e) => setKnowledgeUploadFiles(Array.from(e.target.files || []))}
disabled={uploadingKnowledgeDocuments}
/>
<div className="text-xs text-muted-foreground">
{t('knowledgeDocumentUploadHint')}
</div>
<div className="flex items-center justify-between">
<div className="text-xs text-muted-foreground">
{knowledgeUploadFiles.length > 0
? t('knowledgeDocumentUploadSelected', { count: knowledgeUploadFiles.length })
: t('knowledgeDocumentUploadNone')}
</div>
<Button onClick={handleUploadKnowledgeDocuments} disabled={uploadingKnowledgeDocuments || knowledgeUploadFiles.length === 0}>
{uploadingKnowledgeDocuments ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Plus className="h-4 w-4 mr-2" />}
{t('knowledgeDocumentUploadAction')}
</Button>
</div>
</div>
<div className="grid gap-3">
<div className="space-y-2">
<Label htmlFor="knowledge-doc-title">{t('knowledgeDocumentTitle')}</Label>
<Input
id="knowledge-doc-title"
value={knowledgeDocumentForm.title}
placeholder={t('knowledgeDocumentTitlePlaceholder')}
onChange={(e) => setKnowledgeDocumentForm((prev) => ({ ...prev, title: e.target.value }))}
disabled={isSavingKnowledgeDocument}
/>
</div>
<div className="space-y-2">
<Label htmlFor="knowledge-doc-content">{t('knowledgeDocumentContent')}</Label>
<Textarea
id="knowledge-doc-content"
value={knowledgeDocumentForm.content}
placeholder={t('knowledgeDocumentContentPlaceholder')}
onChange={(e) => setKnowledgeDocumentForm((prev) => ({ ...prev, content: e.target.value }))}
disabled={isSavingKnowledgeDocument}
rows={5}
/>
</div>
<div className="space-y-2">
<Label htmlFor="knowledge-doc-metadata">{t('knowledgeDocumentMetadata')}</Label>
<Textarea
id="knowledge-doc-metadata"
value={knowledgeDocumentForm.metadata}
placeholder={t('knowledgeDocumentMetadataPlaceholder')}
onChange={(e) => setKnowledgeDocumentForm((prev) => ({ ...prev, metadata: e.target.value }))}
disabled={isSavingKnowledgeDocument}
rows={3}
/>
</div>
</div>
<div className="flex items-center justify-end gap-2">
{editingKnowledgeDocumentId ? (
<Button variant="outline" onClick={resetKnowledgeDocumentForm} disabled={isSavingKnowledgeDocument}>
{t('cancel')}
</Button>
) : null}
<Button onClick={handleSaveKnowledgeDocument} disabled={isSavingKnowledgeDocument}>
{isSavingKnowledgeDocument ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
{editingKnowledgeDocumentId ? t('updateKnowledgeDocument') : t('createKnowledgeDocument')}
</Button>
</div>
{isLoadingKnowledgeDocuments ? (
<div className="h-20 flex items-center justify-center text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
</div>
) : knowledgeDocuments.length === 0 ? (
<div className="text-sm text-muted-foreground rounded-md border border-dashed border-border p-4">
{t('noKnowledgeDocuments')}
</div>
) : (
<div className="space-y-2">
{knowledgeDocuments.map((doc) => (
<div key={doc.id} className="rounded-lg border border-border p-3 flex items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<div className="font-medium text-sm text-foreground truncate">{doc.title}</div>
<div className="text-xs text-muted-foreground">
{t('knowledgeDocumentMeta', {
updatedAt: new Date(doc.updated_at).toLocaleString(),
})}
</div>
<div className="text-xs text-muted-foreground break-words">{doc.content.slice(0, 120)}</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<Button variant="ghost" size="icon" onClick={() => handleEditKnowledgeDocument(doc)} title={t('editKnowledgeDocument')}>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
void handleDeleteKnowledgeDocument(doc.id);
}}
disabled={deletingKnowledgeDocumentId === doc.id}
title={t('deleteKnowledgeDocument')}
>
{deletingKnowledgeDocumentId === doc.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4 text-red-500" />}
</Button>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
</CardContent>
<CardFooter className="bg-muted/50/50 border-t border-border pt-6">
<Button variant="outline" onClick={() => void fetchKnowledgeBases()} disabled={!currentProject || isLoadingKnowledgeBases} className="ml-auto">
{isLoadingKnowledgeBases ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-2" />}
{t('refreshKnowledgeBaseList')}
</Button>
</CardFooter>
</Card>