test: 修整测试文件

This commit is contained in:
Wenjie Zhang 2026-03-21 00:03:33 +08:00
parent fa9b708a19
commit 35ccfbc82d
12 changed files with 10 additions and 1092 deletions

View File

@ -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()

View File

@ -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",

View File

@ -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()

View File

@ -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())

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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}")

View File

@ -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