feat: knowledge base first OK
This commit is contained in:
@@ -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}
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
|
||||
Generated
+575
@@ -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"
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "发生错误",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user