test: 修整测试文件
This commit is contained in:
parent
fa9b708a19
commit
35ccfbc82d
@ -81,11 +81,12 @@ class SubAgentRepository:
|
||||
"system_prompt": system_prompt,
|
||||
"tools": tools,
|
||||
}
|
||||
if model_provided:
|
||||
updates["model"] = model
|
||||
for field, value in updates.items():
|
||||
if value is not None:
|
||||
setattr(item, field, value)
|
||||
# model_provided=True 时显式设置 model 值(包括 None),用于清空字段
|
||||
if model_provided:
|
||||
item.model = model
|
||||
item.updated_by = updated_by
|
||||
item.updated_at = utc_now_naive()
|
||||
await self.db.commit()
|
||||
|
||||
@ -106,6 +106,7 @@ members = [
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-v --tb=short"
|
||||
testpaths = ["test"]
|
||||
pythonpath = ["."]
|
||||
markers = [
|
||||
"auth: marks tests that require authentication",
|
||||
"slow: marks tests as slow",
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
"""
|
||||
Quick script to hammer the login endpoint with invalid credentials and observe rate limiting.
|
||||
|
||||
Usage:
|
||||
uv run python test/bruteforce_simulation.py --username demo --attempts 20
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Simulate brute-force login attempts.")
|
||||
parser.add_argument("--base-url", default=os.getenv("TEST_BASE_URL", "http://localhost:5050"), help="API base URL")
|
||||
parser.add_argument("--username", default=os.getenv("TEST_USERNAME", "admin"), help="Login identifier to attack")
|
||||
parser.add_argument("--attempts", type=int, default=20, help="Total number of attempts to issue (default: 20)")
|
||||
parser.add_argument(
|
||||
"--concurrency",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Concurrent request limit (default: 4)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delay",
|
||||
type=float,
|
||||
default=0.05,
|
||||
help="Delay in seconds between scheduling attempts (default: 0.05)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--password",
|
||||
default=None,
|
||||
help="Explicit password to reuse for each request; random values are used when omitted",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def build_payload(username: str, password: str) -> dict[str, str]:
|
||||
return {"username": username, "password": password}
|
||||
|
||||
|
||||
def random_password() -> str:
|
||||
base = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
return "".join(random.choices(base, k=12))
|
||||
|
||||
|
||||
async def attempt_login(
|
||||
client: httpx.AsyncClient,
|
||||
semaphore: asyncio.Semaphore,
|
||||
attempt_no: int,
|
||||
username: str,
|
||||
static_password: str | None,
|
||||
) -> tuple[int, float]:
|
||||
async with semaphore:
|
||||
password = static_password or random_password()
|
||||
payload = build_payload(username, password)
|
||||
started = time.perf_counter()
|
||||
response = await client.post("/api/auth/token", data=payload)
|
||||
elapsed = time.perf_counter() - started
|
||||
detail = (
|
||||
response.json().get("detail")
|
||||
if response.headers.get("content-type", "").startswith("application/json")
|
||||
else response.text
|
||||
)
|
||||
print(
|
||||
f"[{attempt_no:02d}] {response.status_code} in {elapsed * 1000:.1f} ms (pwd={password!r}) detail={detail!r}"
|
||||
)
|
||||
return response.status_code, elapsed
|
||||
|
||||
|
||||
async def run_simulation(args: argparse.Namespace) -> int:
|
||||
timeout = httpx.Timeout(10.0, connect=3.0)
|
||||
limits = httpx.Limits(max_connections=args.concurrency, max_keepalive_connections=args.concurrency)
|
||||
semaphore = asyncio.Semaphore(args.concurrency)
|
||||
status_counts: Counter[int] = Counter()
|
||||
|
||||
async with httpx.AsyncClient(base_url=args.base_url.rstrip("/"), timeout=timeout, limits=limits) as client:
|
||||
tasks = []
|
||||
for attempt_no in range(1, args.attempts + 1):
|
||||
tasks.append(
|
||||
asyncio.create_task(attempt_login(client, semaphore, attempt_no, args.username, args.password))
|
||||
)
|
||||
if args.delay:
|
||||
await asyncio.sleep(args.delay)
|
||||
|
||||
for task in asyncio.as_completed(tasks):
|
||||
status_code, _ = await task
|
||||
status_counts[status_code] += 1
|
||||
|
||||
print("\nSummary:")
|
||||
for code, total in sorted(status_counts.items()):
|
||||
print(f" HTTP {code}: {total} hits")
|
||||
|
||||
if 429 in status_counts:
|
||||
print("Rate limiting engaged (HTTP 429 encountered).")
|
||||
return 0
|
||||
|
||||
print("No rate limiting observed; consider increasing attempt count or reducing delay.")
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
try:
|
||||
exit_code = asyncio.run(run_simulation(args))
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted.")
|
||||
exit_code = 130
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,158 +0,0 @@
|
||||
import asyncio
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
os.environ.setdefault("YUXI_SKIP_APP_INIT", "1")
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from yuxi.config import config
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
from yuxi.storage.postgres.models_knowledge import (
|
||||
EvaluationBenchmark,
|
||||
EvaluationResult,
|
||||
EvaluationResultDetail,
|
||||
KnowledgeBase,
|
||||
KnowledgeFile,
|
||||
)
|
||||
|
||||
|
||||
def _load_json(path: str) -> dict[str, Any]:
|
||||
if not os.path.exists(path):
|
||||
return {}
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JsonState:
|
||||
kb_ids: set[str]
|
||||
file_ids: set[str]
|
||||
benchmark_ids: set[str]
|
||||
result_task_ids: set[str]
|
||||
result_detail_count: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DbState:
|
||||
kb_ids: set[str]
|
||||
file_ids: set[str]
|
||||
benchmark_ids: set[str]
|
||||
result_task_ids: set[str]
|
||||
result_detail_count: int
|
||||
|
||||
|
||||
def load_json_state() -> JsonState:
|
||||
base_dir = os.path.join(config.save_dir, "knowledge_base_data")
|
||||
global_meta = _load_json(os.path.join(base_dir, "global_metadata.json")).get("databases", {}) or {}
|
||||
|
||||
kb_ids: set[str] = set(global_meta.keys())
|
||||
file_ids: set[str] = set()
|
||||
benchmark_ids: set[str] = set()
|
||||
result_task_ids: set[str] = set()
|
||||
result_detail_count = 0
|
||||
|
||||
kb_type_dirs = [
|
||||
p for p in glob.glob(os.path.join(base_dir, "*_data")) if os.path.isdir(p) and os.path.basename(p) != "uploads"
|
||||
]
|
||||
|
||||
for kb_dir in kb_type_dirs:
|
||||
kb_type = os.path.basename(kb_dir)[: -len("_data")]
|
||||
meta_file = os.path.join(kb_dir, f"metadata_{kb_type}.json")
|
||||
meta = _load_json(meta_file)
|
||||
|
||||
databases_meta: dict[str, Any] = meta.get("databases", {}) or {}
|
||||
files_meta: dict[str, Any] = meta.get("files", {}) or {}
|
||||
benchmarks_meta: dict[str, Any] = meta.get("benchmarks", {}) or {}
|
||||
|
||||
kb_ids.update(databases_meta.keys())
|
||||
file_ids.update(files_meta.keys())
|
||||
|
||||
for _db_id, bmap in benchmarks_meta.items():
|
||||
if not isinstance(bmap, dict):
|
||||
continue
|
||||
benchmark_ids.update(bmap.keys())
|
||||
|
||||
for db_id in databases_meta.keys():
|
||||
result_dir = os.path.join(kb_dir, db_id, "results")
|
||||
if not os.path.isdir(result_dir):
|
||||
continue
|
||||
for result_path in glob.glob(os.path.join(result_dir, "*.json")):
|
||||
try:
|
||||
data = _load_json(result_path)
|
||||
except Exception:
|
||||
continue
|
||||
task_id = data.get("task_id") or os.path.splitext(os.path.basename(result_path))[0]
|
||||
result_task_ids.add(task_id)
|
||||
interim = data.get("interim_results") or data.get("results") or []
|
||||
result_detail_count += len(interim)
|
||||
|
||||
return JsonState(
|
||||
kb_ids=kb_ids,
|
||||
file_ids=file_ids,
|
||||
benchmark_ids=benchmark_ids,
|
||||
result_task_ids=result_task_ids,
|
||||
result_detail_count=result_detail_count,
|
||||
)
|
||||
|
||||
|
||||
async def load_db_state() -> DbState:
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
kb_ids = set((await session.execute(select(KnowledgeBase.db_id))).scalars().all())
|
||||
file_ids = set((await session.execute(select(KnowledgeFile.file_id))).scalars().all())
|
||||
benchmark_ids = set((await session.execute(select(EvaluationBenchmark.benchmark_id))).scalars().all())
|
||||
result_task_ids = set((await session.execute(select(EvaluationResult.task_id))).scalars().all())
|
||||
detail_count = (await session.execute(select(func.count(EvaluationResultDetail.id)))).scalar_one()
|
||||
|
||||
return DbState(
|
||||
kb_ids=kb_ids,
|
||||
file_ids=file_ids,
|
||||
benchmark_ids=benchmark_ids,
|
||||
result_task_ids=result_task_ids,
|
||||
result_detail_count=int(detail_count or 0),
|
||||
)
|
||||
|
||||
|
||||
def _diff(name: str, json_set: set[str], db_set: set[str], limit: int = 30) -> list[str]:
|
||||
missing = sorted(json_set - db_set)
|
||||
extra = sorted(db_set - json_set)
|
||||
lines: list[str] = []
|
||||
lines.append(f"{name}: json={len(json_set)} db={len(db_set)}")
|
||||
if missing:
|
||||
preview = ", ".join(missing[:limit])
|
||||
lines.append(f" missing_in_db({len(missing)}): {preview}")
|
||||
if extra:
|
||||
preview = ", ".join(extra[:limit])
|
||||
lines.append(f" extra_in_db({len(extra)}): {preview}")
|
||||
return lines
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
engine_url = pg_manager.async_engine.url.render_as_string(hide_password=True)
|
||||
print(f"db_url={engine_url}")
|
||||
|
||||
json_state = load_json_state()
|
||||
db_state = await load_db_state()
|
||||
|
||||
for line in _diff("knowledge_bases.db_id", json_state.kb_ids, db_state.kb_ids):
|
||||
print(line)
|
||||
for line in _diff("knowledge_files.file_id", json_state.file_ids, db_state.file_ids):
|
||||
print(line)
|
||||
for line in _diff("evaluation_benchmarks.benchmark_id", json_state.benchmark_ids, db_state.benchmark_ids):
|
||||
print(line)
|
||||
for line in _diff("evaluation_results.task_id", json_state.result_task_ids, db_state.result_task_ids):
|
||||
print(line)
|
||||
|
||||
print(
|
||||
"evaluation_result_details: "
|
||||
f"json_count={json_state.result_detail_count} db_count={db_state.result_detail_count}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@ -5,9 +5,15 @@ Shared pytest fixtures for exercising FastAPI routers over the running API servi
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
# Ensure /app is on sys.path so 'server' and 'yuxi' modules resolve correctly.
|
||||
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _root not in sys.path:
|
||||
sys.path.insert(0, _root)
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
@ -1,136 +0,0 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
||||
async def make_request(session: aiohttp.ClientSession, request_id: int) -> dict:
|
||||
"""发送单个请求到API"""
|
||||
url = "http://localhost:5000/chat/call"
|
||||
payload = {"query": "写一个冒泡排序", "meta": {}}
|
||||
|
||||
start_time = time.time()
|
||||
print(f"请求 {request_id} 开始时间: {time.strftime('%H:%M:%S', time.localtime(start_time))}")
|
||||
try:
|
||||
async with session.post(url, json=payload) as response:
|
||||
result = await response.json()
|
||||
print(f"请求 {request_id} 结果: {result}")
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(
|
||||
f"请求 {request_id} 完成时间: "
|
||||
f"{time.strftime('%H:%M:%S', time.localtime(end_time))} "
|
||||
f"(耗时: {duration:.2f}秒)"
|
||||
)
|
||||
return {
|
||||
"request_id": request_id,
|
||||
"status": response.status,
|
||||
"time": duration,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"success": True,
|
||||
}
|
||||
except Exception as e:
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(
|
||||
f"请求 {request_id} 失败时间: "
|
||||
f"{time.strftime('%H:%M:%S', time.localtime(end_time))} "
|
||||
f"(耗时: {duration:.2f}秒)"
|
||||
)
|
||||
return {
|
||||
"request_id": request_id,
|
||||
"status": None,
|
||||
"time": duration,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
|
||||
async def run_concurrent_test(num_requests: int = 10) -> list[dict]:
|
||||
"""运行并发测试"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = [make_request(session, i) for i in range(num_requests)]
|
||||
return await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
def analyze_results(results: list[dict]) -> None:
|
||||
"""分析并打印测试结果"""
|
||||
total_requests = len(results)
|
||||
successful_requests = sum(1 for r in results if r["success"])
|
||||
failed_requests = total_requests - successful_requests
|
||||
|
||||
response_times = [r["time"] for r in results if r["success"]]
|
||||
if response_times:
|
||||
avg_time = sum(response_times) / len(response_times)
|
||||
max_time = max(response_times)
|
||||
min_time = min(response_times)
|
||||
else:
|
||||
avg_time = max_time = min_time = 0
|
||||
|
||||
print("\n=== 并发测试结果 ===")
|
||||
print(f"总请求数: {total_requests}")
|
||||
print(f"成功请求: {successful_requests}")
|
||||
print(f"失败请求: {failed_requests}")
|
||||
print(f"平均响应时间: {avg_time:.2f} 秒")
|
||||
print(f"最长响应时间: {max_time:.2f} 秒")
|
||||
print(f"最短响应时间: {min_time:.2f} 秒")
|
||||
|
||||
if failed_requests > 0:
|
||||
print("\n失败的请求:")
|
||||
for result in results:
|
||||
if not result["success"]:
|
||||
print(f"请求 ID {result['request_id']}: {result.get('error', '未知错误')}")
|
||||
|
||||
# 添加请求时间线分析
|
||||
print("\n=== 请求时间线分析 ===")
|
||||
sorted_results = sorted(results, key=lambda x: x["start_time"])
|
||||
test_start_time = sorted_results[0]["start_time"]
|
||||
test_end_time = sorted_results[-1]["end_time"]
|
||||
print(f"测试开始时间: {time.strftime('%H:%M:%S', time.localtime(test_start_time))}")
|
||||
print(f"测试结束时间: {time.strftime('%H:%M:%S', time.localtime(test_end_time))}")
|
||||
|
||||
print("\n时间线详情:")
|
||||
print("请求ID 开始时间 结束时间 耗时(秒) 重叠请求数")
|
||||
active_requests = []
|
||||
|
||||
for result in sorted_results:
|
||||
# 计算当前时间点的活跃请求数
|
||||
start_time = result["start_time"]
|
||||
end_time = result["end_time"]
|
||||
|
||||
# 清理已完成的请求
|
||||
active_requests = [t for t in active_requests if t > start_time]
|
||||
active_requests.append(end_time)
|
||||
|
||||
print(
|
||||
f"{result['request_id']:^7} "
|
||||
f"{time.strftime('%H:%M:%S', time.localtime(start_time))} "
|
||||
f"{time.strftime('%H:%M:%S', time.localtime(end_time))} "
|
||||
f"{result['time']:^8.2f} {len(active_requests):^6}"
|
||||
)
|
||||
|
||||
# 计算最大并发数
|
||||
max_concurrent = 0
|
||||
timeline = []
|
||||
for r in results:
|
||||
timeline.append((r["start_time"], 1))
|
||||
timeline.append((r["end_time"], -1))
|
||||
|
||||
timeline.sort(key=lambda x: x[0])
|
||||
current_concurrent = 0
|
||||
for _, change in timeline:
|
||||
current_concurrent += change
|
||||
max_concurrent = max(max_concurrent, current_concurrent)
|
||||
|
||||
print(f"\n最大并发请求数: {max_concurrent}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
NUM_REQUESTS = 100 # 设置并发请求数
|
||||
|
||||
print(f"开始运行 {NUM_REQUESTS} 个并发请求的测试...")
|
||||
results = asyncio.run(run_concurrent_test(NUM_REQUESTS))
|
||||
analyze_results(results)
|
||||
@ -1,132 +0,0 @@
|
||||
"""
|
||||
测试知识库删除时 MinIO 文件清理功能
|
||||
|
||||
测试步骤:
|
||||
1. 创建测试知识库
|
||||
2. 上传测试文件到 MinIO(模拟文件上传流程)
|
||||
3. 删除知识库
|
||||
4. 验证 MinIO 文件是否被清理
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
async def test_delete_knowledge_base_cleanup():
|
||||
"""测试删除知识库时 MinIO 文件清理"""
|
||||
from yuxi.storage.minio import get_minio_client
|
||||
from yuxi.knowledge import knowledge_base
|
||||
|
||||
# 初始化
|
||||
minio_client = get_minio_client()
|
||||
kb_manager = knowledge_base
|
||||
doc_bucket = minio_client.KB_BUCKETS["documents"]
|
||||
parsed_bucket = minio_client.KB_BUCKETS["parsed"]
|
||||
|
||||
# 测试参数
|
||||
test_db_name = "test_cleanup_db"
|
||||
test_file_content = b"This is test content for cleanup verification"
|
||||
|
||||
try:
|
||||
# 1. 创建知识库
|
||||
print("1. 创建测试知识库...")
|
||||
kb_info = await kb_manager.create_database(
|
||||
database_name=test_db_name,
|
||||
description="Test knowledge base for MinIO cleanup",
|
||||
kb_type="lightrag",
|
||||
)
|
||||
db_id = kb_info["db_id"]
|
||||
print(f" 知识库创建成功: db_id={db_id}")
|
||||
|
||||
# 2. 上传测试文件到 MinIO(模拟文件上传)
|
||||
print("2. 上传测试文件到 MinIO...")
|
||||
|
||||
# 确保 bucket 存在
|
||||
minio_client.ensure_bucket_exists(doc_bucket)
|
||||
minio_client.ensure_bucket_exists(parsed_bucket)
|
||||
|
||||
# 上传到知识库文档路径 (knowledgebases/{db_id}/upload/...)
|
||||
test_object_name = f"{db_id}/upload/test_file_{db_id[:8]}.txt"
|
||||
minio_client.upload_file(
|
||||
bucket_name=doc_bucket,
|
||||
object_name=test_object_name,
|
||||
data=test_file_content,
|
||||
content_type="text/plain",
|
||||
)
|
||||
print(f" 文件上传到 {doc_bucket}: {test_object_name}")
|
||||
|
||||
# 上传到知识库解析路径 (knowledgebases/{db_id}/parsed/...)
|
||||
test_file_id = f"file_test_{db_id[:8]}"
|
||||
parsed_object_name = f"{db_id}/parsed/{test_file_id}.md"
|
||||
parsed_content = b"# Test Parsed Content\n\nThis is parsed markdown."
|
||||
minio_client.upload_file(
|
||||
bucket_name=parsed_bucket,
|
||||
object_name=parsed_object_name,
|
||||
data=parsed_content,
|
||||
content_type="text/markdown",
|
||||
)
|
||||
print(f" 文件上传到 {parsed_bucket}: {parsed_object_name}")
|
||||
|
||||
# 3. 验证文件存在
|
||||
print("3. 验证文件存在...")
|
||||
assert minio_client.file_exists(doc_bucket, test_object_name), "原始文件应该存在"
|
||||
assert minio_client.file_exists(parsed_bucket, parsed_object_name), "解析后文件应该存在"
|
||||
print(" 文件存在验证通过")
|
||||
|
||||
# 4. 删除知识库
|
||||
print("4. 删除知识库...")
|
||||
result = await kb_manager.delete_database(db_id)
|
||||
print(f" 删除结果: {result}")
|
||||
|
||||
# 5. 验证 MinIO 文件已清理
|
||||
print("5. 验证 MinIO 文件已清理...")
|
||||
|
||||
# 检查文档对象
|
||||
doc_exists = minio_client.file_exists(doc_bucket, test_object_name)
|
||||
print(f" {doc_bucket}/{test_object_name} 存在: {doc_exists}")
|
||||
|
||||
# 检查解析对象
|
||||
parsed_exists = minio_client.file_exists(parsed_bucket, parsed_object_name)
|
||||
print(f" {parsed_bucket}/{parsed_object_name} 存在: {parsed_exists}")
|
||||
|
||||
# 6. 输出测试结果
|
||||
print("\n" + "=" * 50)
|
||||
if not doc_exists and not parsed_exists:
|
||||
print("✅ 测试通过!MinIO 文件已成功清理")
|
||||
return True
|
||||
else:
|
||||
print("❌ 测试失败!以下文件未被清理:")
|
||||
if doc_exists:
|
||||
print(f" - {doc_bucket}/{test_object_name}")
|
||||
if parsed_exists:
|
||||
print(f" - {parsed_bucket}/{parsed_object_name}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试异常: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
finally:
|
||||
# 清理测试数据(确保清理干净)
|
||||
print("\n清理测试数据...")
|
||||
try:
|
||||
# 尝试删除可能残留的文件(忽略错误)
|
||||
for bucket, obj in [(doc_bucket, test_object_name), (parsed_bucket, parsed_object_name)]:
|
||||
try:
|
||||
await minio_client.adelete_file(bucket, obj)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f" 清理时出错: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(test_delete_knowledge_base_cleanup())
|
||||
sys.exit(0 if result else 1)
|
||||
@ -1,212 +0,0 @@
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
# 添加评估指标测试功能
|
||||
def test_evaluation_metrics():
|
||||
"""测试评估指标计算"""
|
||||
print("\n" + "=" * 50)
|
||||
print("测试评估指标计算")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
from yuxi.utils.evaluation_metrics import EvaluationMetricsCalculator
|
||||
|
||||
# 测试检索指标
|
||||
retrieved_chunks = [
|
||||
{"content": "test1", "metadata": {"chunk_id": "file_bbb147_chunk_0"}},
|
||||
{"content": "test2", "metadata": {"chunk_id": "file_bbb147_chunk_1"}},
|
||||
{"content": "test3", "metadata": {"chunk_id": "file_bbb147_chunk_2"}},
|
||||
]
|
||||
gold_chunk_ids = ["file_bbb147_chunk_0", "file_bbb147_chunk_2"]
|
||||
|
||||
print("测试检索指标...")
|
||||
retrieval_metrics = EvaluationMetricsCalculator.calculate_retrieval_metrics(retrieved_chunks, gold_chunk_ids)
|
||||
print(f"检索指标结果: {retrieval_metrics}")
|
||||
|
||||
# 测试答案指标
|
||||
# generated_answer = "该研究以数据语义化—知识结构化—可信推理的技术主线"
|
||||
# gold_answer = "该研究以数据语义化—知识结构化—可信推理的技术主线,遵循数据—知识—推理—应用的演化逻辑"
|
||||
|
||||
print("\n测试答案指标(需要Judge LLM,跳过实际LLM调用)...")
|
||||
# 由于需要judge_llm,这里只测试检索指标
|
||||
print("跳过答案指标测试(需要配置Judge LLM)")
|
||||
|
||||
print("评估指标计算测试完成!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"评估指标测试失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
BASE_URL = "http://localhost:5050"
|
||||
USERNAME = "zwj"
|
||||
PASSWORD = "zwj12138"
|
||||
DB_ID = "kb_5e343066eb4713959698ae6ca16843a0"
|
||||
|
||||
|
||||
def get_token():
|
||||
try:
|
||||
resp = requests.post(f"{BASE_URL}/api/auth/token", data={"username": USERNAME, "password": PASSWORD})
|
||||
if resp.status_code != 200:
|
||||
print(f"Login failed: {resp.text}")
|
||||
return None
|
||||
return resp.json()["access_token"]
|
||||
except Exception as e:
|
||||
print(f"Connection failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
# 首先测试评估指标计算
|
||||
test_evaluation_metrics()
|
||||
|
||||
token = get_token()
|
||||
if not token:
|
||||
sys.exit(1)
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
print(f"\n1. Trying to retrieve a real chunk from {DB_ID}...")
|
||||
|
||||
chunk_content = "This is a fallback test query."
|
||||
chunk_id = "unknown"
|
||||
|
||||
try:
|
||||
# 尝试查询 (Fixed payload structure)
|
||||
resp = requests.post(
|
||||
f"{BASE_URL}/api/knowledge/databases/{DB_ID}/query",
|
||||
headers=headers,
|
||||
json={"query": "人工智能", "meta": {"top_k": 5}},
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
# 结果在 result 字段中
|
||||
results = data.get("result", [])
|
||||
|
||||
first_chunk = None
|
||||
if isinstance(results, list) and len(results) > 0:
|
||||
first_chunk = results[0]
|
||||
elif isinstance(results, dict) and "retrieved_chunks" in results:
|
||||
if results["retrieved_chunks"]:
|
||||
first_chunk = results["retrieved_chunks"][0]
|
||||
|
||||
if first_chunk:
|
||||
print(f"Found chunk: {str(first_chunk.get('content', ''))[:50]}...")
|
||||
chunk_content = first_chunk.get("content", chunk_content)
|
||||
chunk_id = first_chunk.get("chunk_id", chunk_id) or first_chunk.get("id", chunk_id)
|
||||
else:
|
||||
print("No chunks found in retrieval results. Using fallback.")
|
||||
print(f"Raw results: {results}")
|
||||
else:
|
||||
print(f"Query failed: {resp.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Query Exception: {e}")
|
||||
|
||||
print(f"\n2. Creating benchmark with query based on chunk: {chunk_id}")
|
||||
|
||||
# 构造 Benchmark 数据
|
||||
benchmark_data = {
|
||||
"query": chunk_content, # 使用 Chunk 内容作为查询,理论上应该能召回它自己
|
||||
"gold_chunk_ids": [str(chunk_id)], # Ensure string
|
||||
"gold_answer": "This is a gold answer.",
|
||||
}
|
||||
|
||||
# 生成临时文件
|
||||
with open("temp_benchmark.jsonl", "w") as f:
|
||||
f.write(json.dumps(benchmark_data, ensure_ascii=False) + "\n")
|
||||
|
||||
print("\n3. Uploading benchmark...")
|
||||
try:
|
||||
files = {"file": ("temp_benchmark.jsonl", open("temp_benchmark.jsonl", "rb"), "application/jsonlines")}
|
||||
# Fixed: use params for name/description
|
||||
params = {"name": "Manual Eval Test", "description": "Test generated from script"}
|
||||
|
||||
resp = requests.post(
|
||||
f"{BASE_URL}/api/evaluation/databases/{DB_ID}/benchmarks/upload",
|
||||
headers=headers,
|
||||
params=params,
|
||||
files=files,
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
print(f"Upload failed: {resp.text}")
|
||||
sys.exit(1)
|
||||
|
||||
benchmark = resp.json()
|
||||
# Benchmark response format depends on Service impl.
|
||||
# My filesystem impl returns the metadata dict directly.
|
||||
# But wait, KnowledgeRouter might wrap it?
|
||||
# router returns: return {"message": "上传成功", "data": result} (Assume standard wrapper)
|
||||
# Let's check router impl.
|
||||
|
||||
# From router code read earlier:
|
||||
# result = await service.upload_benchmark(...)
|
||||
# return {"message": "上传成功", "data": result}
|
||||
|
||||
benchmark_id = benchmark["data"]["benchmark_id"]
|
||||
print(f"Benchmark uploaded: {benchmark_id}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Upload Exception: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n4. Running evaluation...")
|
||||
try:
|
||||
payload = {"benchmark_id": benchmark_id, "retrieval_config": {"top_k": 5}}
|
||||
|
||||
resp = requests.post(f"{BASE_URL}/api/evaluation/databases/{DB_ID}/run", headers=headers, json=payload)
|
||||
|
||||
if resp.status_code != 200:
|
||||
print(f"Run evaluation failed: {resp.text}")
|
||||
sys.exit(1)
|
||||
|
||||
task_id = resp.json()["data"]["task_id"]
|
||||
print(f"Evaluation task started: {task_id}")
|
||||
|
||||
# 轮询状态
|
||||
while True:
|
||||
resp = requests.get(f"{BASE_URL}/api/evaluation/{task_id}/progress", headers=headers)
|
||||
if resp.status_code != 200:
|
||||
print(f"Get progress failed: {resp.text}")
|
||||
break
|
||||
|
||||
progress = resp.json()
|
||||
# The progress endpoint returns {task_id, status, ...} based on my service impl?
|
||||
# Router wrapper: return {"message": "success", "data": result}
|
||||
|
||||
data = progress.get("data", progress) # Handle wrapper if exists
|
||||
status = data["status"]
|
||||
current_progress = data.get("progress", 0)
|
||||
|
||||
print(f"Status: {status}, Progress: {current_progress}%")
|
||||
|
||||
if status in ["completed", "failed"]:
|
||||
break
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
if status == "completed":
|
||||
print("\nEvaluation Completed!")
|
||||
# 获取结果
|
||||
resp = requests.get(f"{BASE_URL}/api/evaluation/{task_id}/results", headers=headers)
|
||||
print(json.dumps(resp.json(), indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print("\nEvaluation Failed!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Evaluation Exception: {e}")
|
||||
|
||||
# 清理
|
||||
if os.path.exists("temp_benchmark.jsonl"):
|
||||
os.remove("temp_benchmark.jsonl")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,206 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MySQL 数据库连接验证脚本
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
# sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent.parent))
|
||||
|
||||
|
||||
def load_env_file(env_file=".env"):
|
||||
"""加载 .env 文件"""
|
||||
env_vars = {}
|
||||
if os.path.exists(env_file):
|
||||
with open(env_file, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
env_vars[key] = value.strip("\"'")
|
||||
return env_vars
|
||||
|
||||
|
||||
def test_mysql_connection():
|
||||
"""测试 MySQL 连接"""
|
||||
print("=== MySQL 数据库连接验证 ===\n")
|
||||
|
||||
# 加载环境变量
|
||||
env_vars = load_env_file()
|
||||
|
||||
# 检查必需的环境变量
|
||||
required_vars = ["MYSQL_HOST", "MYSQL_USER", "MYSQL_PASSWORD", "MYSQL_DATABASE"]
|
||||
missing_vars = []
|
||||
|
||||
for var in required_vars:
|
||||
if var not in env_vars:
|
||||
missing_vars.append(var)
|
||||
|
||||
if missing_vars:
|
||||
print(f"❌ 缺少必需的环境变量: {', '.join(missing_vars)}")
|
||||
print("\n请在 .env 文件中设置以下变量:")
|
||||
for var in missing_vars:
|
||||
print(f" {var}=your_value")
|
||||
return False
|
||||
|
||||
# 显示配置信息(隐藏密码)
|
||||
print("📋 数据库配置:")
|
||||
print(f" Host: {env_vars.get('MYSQL_HOST', 'Not set')}")
|
||||
print(f" User: {env_vars.get('MYSQL_USER', 'Not set')}")
|
||||
print(f" Database: {env_vars.get('MYSQL_DATABASE', 'Not set')}")
|
||||
print(f" Port: {env_vars.get('MYSQL_PORT', '3306')}")
|
||||
print(f" Charset: {env_vars.get('MYSQL_CHARSET', 'utf8mb4')}")
|
||||
print()
|
||||
|
||||
try:
|
||||
# 导入 MySQL 连接管理器
|
||||
from yuxi.agents.toolkits.mysql.connection import MySQLConnectionManager
|
||||
|
||||
# 创建连接配置
|
||||
mysql_config = {
|
||||
"host": env_vars["MYSQL_HOST"],
|
||||
"user": env_vars["MYSQL_USER"],
|
||||
"password": env_vars["MYSQL_PASSWORD"],
|
||||
"database": env_vars["MYSQL_DATABASE"],
|
||||
"port": int(env_vars.get("MYSQL_PORT", "3306")),
|
||||
"charset": env_vars.get("MYSQL_CHARSET", "utf8mb4"),
|
||||
}
|
||||
|
||||
# 创建连接管理器
|
||||
print("🔄 正在连接数据库...")
|
||||
conn_manager = MySQLConnectionManager(mysql_config)
|
||||
|
||||
# 测试连接
|
||||
with conn_manager.get_cursor() as cursor:
|
||||
# 测试基本连接
|
||||
cursor.execute("SELECT 1 as test")
|
||||
result = cursor.fetchone()
|
||||
if result and result["test"] == 1:
|
||||
print("✅ 数据库连接成功!")
|
||||
|
||||
# 获取数据库版本
|
||||
cursor.execute("SELECT VERSION() as version")
|
||||
version_info = cursor.fetchone()
|
||||
print(f"📊 MySQL 版本: {version_info['version']}")
|
||||
|
||||
# 获取当前数据库
|
||||
cursor.execute("SELECT DATABASE() as db_name")
|
||||
db_info = cursor.fetchone()
|
||||
print(f"🗄️ 当前数据库: {db_info['db_name']}")
|
||||
|
||||
# 获取表数量
|
||||
cursor.execute("SHOW TABLES")
|
||||
tables = cursor.fetchall()
|
||||
print(f"📋 表数量: {len(tables)}")
|
||||
|
||||
if tables:
|
||||
print("\n📝 数据库表列表:")
|
||||
for i, table in enumerate(tables[:10]): # 只显示前10个表
|
||||
table_name = list(table.values())[0]
|
||||
print(f" {i + 1}. {table_name}")
|
||||
|
||||
if len(tables) > 10:
|
||||
print(f" ... 还有 {len(tables) - 10} 个表未显示")
|
||||
|
||||
return True
|
||||
else:
|
||||
print("❌ 数据库连接测试失败")
|
||||
return False
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ 导入错误: {e}")
|
||||
print("请确保已安装 pymysql 依赖")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 连接失败: {e}")
|
||||
|
||||
# 提供故障排除建议
|
||||
print("\n💡 故障排除建议:")
|
||||
print("1. 检查数据库服务是否正在运行")
|
||||
print("2. 验证主机地址和端口是否正确")
|
||||
print("3. 确认用户名和密码是否正确")
|
||||
print("4. 检查数据库是否存在")
|
||||
print("5. 确认网络连接和防火墙设置")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def test_tools():
|
||||
"""测试 MySQL 工具是否正常工作"""
|
||||
print("\n=== MySQL 工具测试 ===\n")
|
||||
|
||||
try:
|
||||
from yuxi.agents.toolkits.mysql.tools import mysql_list_tables, mysql_describe_table, mysql_query
|
||||
|
||||
print("✅ MySQL 工具导入成功")
|
||||
|
||||
# 测试获取表名
|
||||
print("\n🔄 测试获取表名...")
|
||||
result = mysql_list_tables.invoke({})
|
||||
if "失败" not in result and "错误" not in result:
|
||||
print("✅ 获取表名工具正常")
|
||||
else:
|
||||
print(f"❌ 获取表名工具异常: {result}")
|
||||
return False
|
||||
|
||||
# 如果有表,测试获取表结构
|
||||
if "数据库中的表:" in result:
|
||||
print("\n🔄 测试获取表结构...")
|
||||
# 提取第一个表名
|
||||
lines = result.split("\n")
|
||||
for line in lines:
|
||||
if "- " in line and "(" in line:
|
||||
table_name = line.split("- ")[1].split(" ")[0]
|
||||
break
|
||||
else:
|
||||
table_name = None
|
||||
|
||||
if table_name:
|
||||
structure_result = mysql_describe_table.invoke({"table_name": table_name})
|
||||
if "失败" not in structure_result and "错误" not in structure_result:
|
||||
print("✅ 获取表结构工具正常")
|
||||
else:
|
||||
print(f"❌ 获取表结构工具异常: {structure_result}")
|
||||
return False
|
||||
|
||||
# 测试简单查询
|
||||
print("\n🔄 测试SQL查询...")
|
||||
query_result = mysql_query.invoke({"sql": f"SELECT COUNT(*) as total FROM `{table_name}`"})
|
||||
if "失败" not in query_result and "错误" not in query_result:
|
||||
print("✅ SQL查询工具正常")
|
||||
else:
|
||||
print(f"❌ SQL查询工具异常: {query_result}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 工具测试失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("MySQL 数据库连接和工具验证脚本")
|
||||
print("=" * 50)
|
||||
|
||||
# 测试连接
|
||||
connection_ok = test_mysql_connection()
|
||||
|
||||
if connection_ok:
|
||||
# 测试工具
|
||||
tools_ok = test_tools()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
if connection_ok and tools_ok:
|
||||
print("🎉 所有测试通过!MySQL 工具包可以正常使用")
|
||||
else:
|
||||
print("❌ 部分测试失败,请检查配置")
|
||||
else:
|
||||
print("\n" + "=" * 50)
|
||||
print("❌ 数据库连接失败,请检查配置")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
简单测试 MySQL 工具导入
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
try:
|
||||
from yuxi.agents.toolkits.mysql.tools import mysql_list_tables, mysql_describe_table, mysql_query
|
||||
|
||||
print("✅ MySQL 工具导入成功")
|
||||
print(f"工具列表: {[tool.name for tool in [mysql_list_tables, mysql_describe_table, mysql_query]]}")
|
||||
|
||||
# 检查工具描述
|
||||
for tool in [mysql_list_tables, mysql_describe_table, mysql_query]:
|
||||
print(f" - {tool.name}: {tool.description[:60]}...")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 导入失败: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
@ -1,37 +0,0 @@
|
||||
from neo4j import GraphDatabase
|
||||
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||
|
||||
# sudo ln -s /snap/core22/1586/usr/sbin/iptables /usr/sbin/iptables
|
||||
|
||||
|
||||
def check_neo4j_status(uri="bolt://localhost:7687", username="neo4j", password="0123456789"):
|
||||
"""
|
||||
检查 Neo4j 数据库是否可以连接并正常工作。
|
||||
|
||||
参数:
|
||||
uri (str): Neo4j 的 URI,默认为 "bolt://localhost:7687"
|
||||
username (str): 数据库用户名,默认为 "neo4j"
|
||||
password (str): 数据库密码,默认为 "0123456789"
|
||||
|
||||
返回:
|
||||
str: "OK" 表示连接成功,"UNAVAILABLE" 表示服务不可用,"AUTH_FAILED" 表示认证失败。
|
||||
"""
|
||||
try:
|
||||
driver = GraphDatabase.driver(uri, auth=(username, password))
|
||||
with driver.session() as session:
|
||||
# 简单的查询来测试连接
|
||||
result = session.run("RETURN 1")
|
||||
if result.single()[0] == 1:
|
||||
return "OK"
|
||||
except ServiceUnavailable:
|
||||
return "UNAVAILABLE"
|
||||
except AuthError:
|
||||
return "AUTH_FAILED"
|
||||
finally:
|
||||
# 确保关闭驱动
|
||||
driver.close()
|
||||
|
||||
|
||||
# 测试函数
|
||||
status = check_neo4j_status()
|
||||
print(f"Neo4j status: {status}")
|
||||
@ -457,26 +457,6 @@ class TestSubAgentService:
|
||||
assert second[0]["tools"] == ["tool_a"]
|
||||
service_module.clear_specs_cache()
|
||||
|
||||
def test_resolve_subagent_tools_does_not_mutate_input(self):
|
||||
from yuxi.services import subagent_service as service_module
|
||||
|
||||
mock_tool = MagicMock()
|
||||
mock_tool.name = "tool_a"
|
||||
specs = [
|
||||
{
|
||||
"name": "test-agent",
|
||||
"description": "Test",
|
||||
"system_prompt": "You are a test",
|
||||
"tools": ["tool_a"],
|
||||
}
|
||||
]
|
||||
|
||||
resolved = service_module.resolve_subagent_tools(specs, [mock_tool])
|
||||
|
||||
assert specs[0]["tools"] == ["tool_a"]
|
||||
assert resolved[0]["tools"] == [mock_tool]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Model Tests
|
||||
# =============================================================================
|
||||
@ -549,49 +529,6 @@ class TestSubAgentModel:
|
||||
|
||||
|
||||
class TestDeepAgentSubagentSelection:
|
||||
def test_filter_specs_by_names_empty_selection_returns_empty(self):
|
||||
from yuxi.services.subagent_service import filter_specs_by_names
|
||||
|
||||
specs = [
|
||||
{"name": "research-agent", "description": "r"},
|
||||
{"name": "critique-agent", "description": "c"},
|
||||
]
|
||||
|
||||
filtered, missing = filter_specs_by_names(specs, [])
|
||||
|
||||
assert filtered == []
|
||||
assert missing == []
|
||||
|
||||
def test_filter_specs_by_names_none_selection_returns_all(self):
|
||||
from yuxi.services.subagent_service import filter_specs_by_names
|
||||
|
||||
specs = [
|
||||
{"name": "research-agent", "description": "r"},
|
||||
{"name": "critique-agent", "description": "c"},
|
||||
]
|
||||
|
||||
filtered, missing = filter_specs_by_names(specs, None)
|
||||
|
||||
assert filtered == specs
|
||||
assert missing == []
|
||||
|
||||
def test_filter_specs_by_names_returns_subset_and_missing(self):
|
||||
from yuxi.services.subagent_service import filter_specs_by_names
|
||||
|
||||
specs = [
|
||||
{"name": "research-agent", "description": "r"},
|
||||
{"name": "critique-agent", "description": "c"},
|
||||
{"name": 123, "description": "invalid"},
|
||||
]
|
||||
|
||||
filtered, missing = filter_specs_by_names(
|
||||
specs,
|
||||
["research-agent", "missing-agent", "research-agent", ""],
|
||||
)
|
||||
|
||||
assert [item["name"] for item in filtered] == ["research-agent"]
|
||||
assert missing == ["missing-agent"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_subagents_from_names_filters_and_resolves_tools(self, monkeypatch):
|
||||
from yuxi.services import subagent_service as service_module
|
||||
|
||||
Loading…
Reference in New Issue
Block a user