Compare commits

..

No commits in common. "KuangZhi" and "main" have entirely different histories.

54 changed files with 9298 additions and 2897 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
docker-images/*.tar.gz filter=lfs diff=lfs merge=lfs -text

33
.gitignore vendored
View File

@ -1,33 +1,16 @@
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class .pytest_cache/
.cache/
.Python
.venv/
venv/
env/
ENV/
*.swp
*.bak*
*.before_rollback_*
*.tmp
*.log *.log
*.tmp
config.py *.swp
config.txt .DS_Store
.nfs*
data/*.db
data/*.db-*
data/*.sqlite
data/*.sqlite-*
created_pdf/ created_pdf/
created_word/ created_word/
image_output/ image_output/
audio/
workflows/=* data/
.pytest_cache/
.mypy_cache/
.ruff_cache/

BIN
Python-3.12.12.tgz Normal file

Binary file not shown.

View File

@ -0,0 +1,38 @@
# WX Agent 镜像和代码备份
来源设备:`192.168.1.108`
原始代码路径:`/home/workspace/wxagent`
镜像名称:`wx-agent:latest`
## 主要内容
- `app.py`Agent 服务入口
- `main_agent.py`Agent 主流程
- `config.py`:当前配置文件
- `requirements.txt`Python 依赖
- `docker-images/wx_agent_latest.docker-save.tar.gz``wx-agent:latest` 镜像备份
## 已清理内容
本仓库未上传生成文件、音频/图片输出、运行数据、缓存,以及 `.bak` 或带日期后缀的历史备份文件。
## 大文件下载
本仓库使用 Git LFS 保存 Docker 镜像包。
克隆仓库后执行:
```bash
git lfs install
git lfs pull
```
如果镜像文件内容是 `version https://git-lfs.github.com/spec/v1`,说明当前只是 LFS 指针文件,还需要执行 `git lfs pull` 下载真实文件。
## 恢复镜像
```bash
gunzip -c docker-images/wx_agent_latest.docker-save.tar.gz | docker load
```

119
app.py
View File

@ -701,19 +701,6 @@ async def fault_type_statistics(ship_number: Optional[str] = None):
detail=result.get("error", "统计失败") detail=result.get("error", "统计失败")
) )
# result = {
# "success": True,
# "faultTypeData": [
# { "value": 35, "name": "作战系统", "itemStyle": { "color": "#00C2FF" } },
# { "value": 25, "name": "电力系统", "itemStyle": { "color": "#FF4500" } },
# { "value": 20, "name": "推进系统", "itemStyle": { "color": "#9400D3" } },
# { "value": 10, "name": "航空保障系统", "itemStyle": { "color": "#228B22" } },
# { "value": 5, "name": "船舶保障系统", "itemStyle": { "color": "#FF1493" } },
# { "value": 5, "name": "其他", "itemStyle": { "color": "#2C2C2C" } }
# ]
# }
return result return result
@ -743,64 +730,6 @@ async def fault_frequency_statistics(
start_date = None start_date = None
end_date = None end_date = None
# result = {
# "success": True,
# "highFreqFaults": [
# { "rank": 1, "name": "燃气轮机燃油增压泵组泵体旋塞处泄漏", "system": "推进系统", "count": 135 },
# { "rank": 2, "name": "气动蝶阀控制箱无法上电工作", "system": "推进系统", "count": 103 },
# { "rank": 3, "name": "组合报警灯柱不能正常工作", "system": "船舶保障系统", "count": 86 },
# { "rank": 4, "name": "岸电联接箱指示灯不亮", "system": "电力系统", "count": 77 },
# { "rank": 5, "name": "主机冷却水泵密封泄漏", "system": "航空保障系统", "count": 76 },
# { "rank": 6, "name": "加电后屏幕和故障指示灯没显示", "system": "作战系统", "count": 65 }
# ]
# }
# if time_range == "近七天":
# end_date = datetime.now().strftime("%Y-%m-%d")
# start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
# result = {
# "success": True,
# "highFreqFaults": [
# { "rank": 1, "name": "气动蝶阀控制箱无法上电工作", "system": "推进系统", "count": 10 },
# { "rank": 2, "name": "组合报警灯柱不能正常工作", "system": "船舶保障系统", "count": 6 },
# { "rank": 3, "name": "岸电联接箱指示灯不亮", "system": "电力系统", "count": 3 },
# { "rank": 4, "name": "主机冷却水泵密封泄漏", "system": "航空保障系统", "count": 3 },
# { "rank": 5, "name": "加电后屏幕和故障指示灯没显示", "system": "作战系统", "count": 1 }
# ]
# }
# elif time_range == "近一个月":
# end_date = datetime.now().strftime("%Y-%m-%d")
# start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
# result = {
# "success": True,
# "highFreqFaults": [
# { "rank": 1, "name": "燃气轮机燃油增压泵组泵体旋塞处泄漏", "system": "推进系统", "count": 38 },
# { "rank": 2, "name": "气动蝶阀控制箱无法上电工作", "system": "推进系统", "count": 20 },
# { "rank": 3, "name": "组合报警灯柱不能正常工作", "system": "船舶保障系统", "count": 14 },
# { "rank": 4, "name": "岸电联接箱指示灯不亮", "system": "电力系统", "count": 13 },
# { "rank": 5, "name": "主机冷却水泵密封泄漏", "system": "航空保障系统", "count": 10 },
# { "rank": 6, "name": "加电后屏幕和故障指示灯没显示", "system": "作战系统", "count": 8 }
# ]
# }
# elif time_range == "近一年":
# end_date = datetime.now().strftime("%Y-%m-%d")
# start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")
# result = {
# "success": True,
# "highFreqFaults": [
# { "rank": 1, "name": "燃气轮机燃油增压泵组泵体旋塞处泄漏", "system": "推进系统", "count": 68 },
# { "rank": 2, "name": "气动蝶阀控制箱无法上电工作", "system": "推进系统", "count": 74 },
# { "rank": 3, "name": "组合报警灯柱不能正常工作", "system": "船舶保障系统", "count": 73 },
# { "rank": 4, "name": "岸电联接箱指示灯不亮", "system": "电力系统", "count": 63 },
# { "rank": 5, "name": "主机冷却水泵密封泄漏", "system": "航空保障系统", "count": 56 },
# { "rank": 6, "name": "加电后屏幕和故障指示灯没显示", "system": "作战系统", "count": 46 }
# ]
# }
if time_range == "近七天": if time_range == "近七天":
end_date = datetime.now().strftime("%Y-%m-%d") end_date = datetime.now().strftime("%Y-%m-%d")
start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
@ -856,54 +785,6 @@ async def spare_parts_statistics(
start_date = None start_date = None
end_date = None end_date = None
# result = {
# "success": True,
# "sparePartData": {
# "categories": ["螺旋式熔断体", "断路器", "机械密封", "门铰链固定端", "减振器"],
# "system": ["电力系统", "船舶保障系统", "推进系统", "作战系统", "推进系统"],
# "counter": [86, 76, 43, 42, 33]
# }
# }
# if time_range == "近七天":
# end_date = datetime.now().strftime("%Y-%m-%d")
# start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
# result = {
# "success": True,
# "sparePartData": {
# "categories": ["螺旋式熔断体", "断路器", "机械密封", "门铰链固定端"],
# "system": ["电力系统", "船舶保障系统", "推进系统", "作战系统"],
# "counter": [13, 9, 8, 5]
# }
# }
# elif time_range == "近一个月":
# end_date = datetime.now().strftime("%Y-%m-%d")
# start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
# result = {
# "success": True,
# "sparePartData": {
# "categories": ["螺旋式熔断体", "断路器", "机械密封", "门铰链固定端", "减振器"],
# "system": ["电力系统", "船舶保障系统", "推进系统", "作战系统", "推进系统"],
# "counter": [32, 26, 20, 14, 10]
# }
# }
# elif time_range == "近一年":
# end_date = datetime.now().strftime("%Y-%m-%d")
# start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")
# result = {
# "success": True,
# "sparePartData": {
# "categories": ["螺旋式熔断体", "断路器", "机械密封", "门铰链固定端", "减振器"],
# "system": ["电力系统", "船舶保障系统", "推进系统", "作战系统", "推进系统"],
# "counter": [64, 63, 23, 20, 19]
# }
# }
if time_range == "近七天": if time_range == "近七天":
end_date = datetime.now().strftime("%Y-%m-%d") end_date = datetime.now().strftime("%Y-%m-%d")
start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")

1010
app63.py Normal file

File diff suppressed because it is too large Load Diff

1012
app8964.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

493
checkpointer_config521.py Normal file
View File

@ -0,0 +1,493 @@
"""
LangGraph Checkpointer 配置模块
支持多种持久化后端
1. MemorySaver - 内存存储仅用于测试
2. PostgresSaver - PostgreSQL 数据库生产环境
使用 message_id chat_id 作为 thread_id 来恢复工作流状态
"""
import os
import asyncio
from typing import Optional
from langgraph.checkpoint.memory import MemorySaver
from config import POSTGRES_CONNECTION_STRING
CHECKPOINTER_TYPE = "postgres"
def get_thread_id(chat_id: str, message_id: str) -> str:
"""
生成唯一的 thread_id
Args:
chat_id: 聊天会话 ID
message_id: 消息 ID
Returns:
组合后的唯一 thread_id
"""
return f"{chat_id}:{message_id}"
def parse_thread_id(thread_id: str) -> tuple[str, str]:
"""
解析 thread_id 获取 chat_id message_id
Args:
thread_id: 组合的 thread_id
Returns:
(chat_id, message_id) 元组
"""
parts = thread_id.split(":", 1)
if len(parts) == 2:
return parts[0], parts[1]
return parts[0], ""
async def verify_checkpointer_tables_exist() -> bool:
"""
验证 checkpointer 所需的数据库表是否存在且结构正确
Returns:
True 如果所有必需的表都存在且结构正确否则 False
"""
try:
from psycopg_pool import AsyncConnectionPool
except ImportError:
return False
try:
async with AsyncConnectionPool(
POSTGRES_CONNECTION_STRING,
kwargs={"autocommit": True}
) as pool:
async with pool.connection() as conn:
async with conn.cursor() as cur:
# 检查 checkpoints 表
await cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'checkpoints'
)
""")
checkpoints_exists = (await cur.fetchone())[0]
# 检查 checkpoint_writes 表
await cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'checkpoint_writes'
)
""")
writes_exists = (await cur.fetchone())[0]
# 检查 checkpoint_blobs 表
await cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'checkpoint_blobs'
)
""")
blobs_exists = (await cur.fetchone())[0]
if not (checkpoints_exists and writes_exists and blobs_exists):
return False
# 检查 checkpoints 表的 checkpoint 字段是否为 JSONB 类型
await cur.execute("""
SELECT data_type
FROM information_schema.columns
WHERE table_name = 'checkpoints'
AND column_name = 'checkpoint'
""")
result = await cur.fetchone()
if not result or result[0] != 'jsonb':
print(f"[Checkpointer] checkpoints.checkpoint column type is {result[0] if result else 'NULL'}, expected 'jsonb'")
return False
# 检查 checkpoint_writes 表是否有 task_path 列
await cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = 'checkpoint_writes'
AND column_name = 'task_path'
)
""")
task_path_exists = (await cur.fetchone())[0]
if not task_path_exists:
print(f"[Checkpointer] checkpoint_writes.task_path column does not exist")
return False
# 检查 checkpoint_writes 表的主键是否正确
await cur.execute("""
SELECT COUNT(*)
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = 'checkpoint_writes'
AND tc.constraint_type = 'PRIMARY KEY'
""")
pk_count = (await cur.fetchone())[0]
if pk_count != 5: # 应该有 5 个主键列
print(f"[Checkpointer] checkpoint_writes has {pk_count} primary key columns, expected 5")
return False
return True
except Exception as e:
print(f"[Checkpointer] Error verifying tables: {e}")
return False
async def init_checkpointer_db():
"""
初始化 PostgreSQL checkpointer 数据库表
使用 AsyncPostgresSaver setup 方法创建正确的表结构
"""
if CHECKPOINTER_TYPE != "postgres":
return
try:
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from psycopg_pool import AsyncConnectionPool
import psycopg
except ImportError as e:
print(f"[Checkpointer] ERROR: Missing required package: {e}")
print(f"[Checkpointer] Please install: pip install langgraph-checkpoint-postgres psycopg-pool")
raise ImportError(
"langgraph-checkpoint-postgres or psycopg-pool not installed. "
"Run: pip install langgraph-checkpoint-postgres psycopg-pool"
) from e
# 首先检查表是否已存在
tables_exist = await verify_checkpointer_tables_exist()
if tables_exist:
print(f"[Checkpointer] Database tables already exist, skipping initialization")
return
print(f"[Checkpointer] Initializing PostgreSQL database tables...")
# 尝试使用 AsyncPostgresSaver 的 setup 方法
try:
async with AsyncConnectionPool(
POSTGRES_CONNECTION_STRING,
kwargs={"autocommit": True}
) as pool:
checkpointer = AsyncPostgresSaver(pool)
await checkpointer.setup()
print(f"[Checkpointer] AsyncPostgresSaver.setup() executed")
except Exception as e:
print(f"[Checkpointer] AsyncPostgresSaver.setup() failed: {e}")
# 验证表是否真的被创建
tables_exist_after = await verify_checkpointer_tables_exist()
if not tables_exist_after:
print(f"[Checkpointer] Tables not created by setup(), using direct SQL...")
try:
await create_checkpointer_tables_directly()
except Exception as e2:
print(f"[Checkpointer] Direct SQL initialization also failed: {e2}")
raise
else:
print(f"[Checkpointer] PostgreSQL database tables verified successfully")
async def create_checkpointer_tables_directly():
"""
直接通过 SQL 语句创建 checkpointer 所需的数据库表
这是备用方案 AsyncPostgresSaver.setup() 失败时使用
注意LangGraph AsyncPostgresSaver 使用 JSONB 类型存储数据
表名必须是: checkpoints, checkpoint_writes, checkpoint_blobs
"""
try:
from psycopg_pool import AsyncConnectionPool
except ImportError as e:
print(f"[Checkpointer] ERROR: Missing psycopg-pool: {e}")
raise
print(f"[Checkpointer] Creating checkpointer tables using direct SQL...")
async with AsyncConnectionPool(
POSTGRES_CONNECTION_STRING,
kwargs={"autocommit": True}
) as pool:
async with pool.connection() as conn:
async with conn.cursor() as cur:
# 先删除旧表(如果存在错误的表结构)
await cur.execute("DROP TABLE IF EXISTS checkpoints CASCADE")
await cur.execute("DROP TABLE IF EXISTS writes CASCADE")
await cur.execute("DROP TABLE IF EXISTS checkpoint_blobs CASCADE")
await cur.execute("DROP TABLE IF EXISTS checkpoint_writes CASCADE")
await cur.execute("DROP TABLE IF EXISTS blobs CASCADE")
await cur.execute("DROP TABLE IF EXISTS checkpoint_migrations CASCADE")
print(f"[Checkpointer] Dropped existing tables")
# 创建 checkpoint_migrations 表(用于跟踪迁移版本)
await cur.execute("""
CREATE TABLE IF NOT EXISTS checkpoint_migrations (
v INTEGER PRIMARY KEY
)
""")
# 创建 checkpoints 表 - 使用 JSONB 类型
await cur.execute("""
CREATE TABLE IF NOT EXISTS checkpoints (
thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL DEFAULT '',
checkpoint_id TEXT NOT NULL,
parent_checkpoint_id TEXT,
type TEXT,
checkpoint JSONB NOT NULL DEFAULT '{}',
metadata JSONB NOT NULL DEFAULT '{}',
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
)
""")
# 创建 checkpoint_writes 表 - 使用 BYTEA 类型
await cur.execute("""
CREATE TABLE IF NOT EXISTS checkpoint_writes (
thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL DEFAULT '',
checkpoint_id TEXT NOT NULL,
task_id TEXT NOT NULL,
task_path TEXT NOT NULL DEFAULT '',
idx INTEGER NOT NULL,
channel TEXT NOT NULL,
type TEXT,
blob BYTEA,
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
)
""")
# 创建 checkpoint_blobs 表 - 使用 BYTEA 类型存储二进制数据
await cur.execute("""
CREATE TABLE IF NOT EXISTS checkpoint_blobs (
thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL DEFAULT '',
channel TEXT NOT NULL,
version TEXT NOT NULL,
type TEXT NOT NULL,
blob BYTEA,
PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
)
""")
# 创建索引以提高查询性能
await cur.execute("""
CREATE INDEX IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id)
""")
await cur.execute("""
CREATE INDEX IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id)
""")
await cur.execute("""
CREATE INDEX IF NOT EXISTS checkpoint_blobs_thread_id_idx ON checkpoint_blobs(thread_id)
""")
# 插入迁移版本记录
await cur.execute("""
INSERT INTO checkpoint_migrations (v) VALUES (1) ON CONFLICT DO NOTHING
""")
print(f"[Checkpointer] Checkpointer tables created successfully via direct SQL")
def create_checkpointer(checkpointer_type: str = None):
"""
创建同步 checkpointer 实例
Args:
checkpointer_type: checkpointer 类型可选 "memory", "postgres"
Returns:
checkpointer 实例
"""
checkpointer_type = checkpointer_type or CHECKPOINTER_TYPE
if checkpointer_type == "memory":
return MemorySaver()
elif checkpointer_type == "postgres":
try:
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg import Connection
if not POSTGRES_CONNECTION_STRING:
raise ValueError("POSTGRES_CONNECTION_STRING 环境变量未设置")
conn = Connection.connect(POSTGRES_CONNECTION_STRING)
return PostgresSaver(conn)
except ImportError:
print("[WARNING] psycopg 或 langgraph.checkpoint.postgres 未安装,回退到 memory")
return MemorySaver()
else:
print(f"[WARNING] 未知的 checkpointer 类型: {checkpointer_type},使用 memory")
return MemorySaver()
def create_async_checkpointer(checkpointer_type: str = None):
"""
创建异步 checkpointer 实例
Args:
checkpointer_type: checkpointer 类型
Returns:
异步 checkpointer 实例
"""
checkpointer_type = checkpointer_type or CHECKPOINTER_TYPE
print(f"[Checkpointer] 创建异步 checkpointer, 类型配置: {checkpointer_type}")
if checkpointer_type == "memory":
print("[Checkpointer] 使用 MemorySaver")
return MemorySaver()
elif checkpointer_type == "postgres":
try:
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from psycopg_pool import AsyncConnectionPool
if not POSTGRES_CONNECTION_STRING:
raise ValueError("POSTGRES_CONNECTION_STRING 环境变量未设置")
pool = AsyncConnectionPool(POSTGRES_CONNECTION_STRING)
return AsyncPostgresSaver(pool)
except ImportError:
print("[WARNING] psycopg_pool 或 langgraph.checkpoint.postgres 未安装,回退到 memory")
return MemorySaver()
else:
print(f"[WARNING] 未知的 checkpointer 类型: {checkpointer_type},使用 memory")
return MemorySaver()
async def get_or_create_async_checkpointer(checkpointer_type: str = None):
"""
获取或创建异步 checkpointer 实例正确处理上下文管理器
Args:
checkpointer_type: checkpointer 类型
Returns:
已初始化的异步 checkpointer 实例
"""
checkpointer = create_async_checkpointer(checkpointer_type)
if hasattr(checkpointer, '__aenter__'):
checkpointer = await checkpointer.__aenter__()
return checkpointer
class CheckpointerManager:
"""
Checkpointer 管理器 - 单例模式
用于管理多个工作流的 checkpointer 实例
"""
_instance = None
_sync_checkpointer = None
_async_conn_pool = None
_async_checkpointer = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def get_checkpointer(self):
"""获取同步 checkpointer 实例"""
if self._sync_checkpointer is None:
self._sync_checkpointer = create_checkpointer()
return self._sync_checkpointer
async def get_async_checkpointer(self):
"""
获取异步 checkpointer 实例
"""
if self._async_checkpointer is None:
if CHECKPOINTER_TYPE == "postgres":
try:
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from psycopg_pool import AsyncConnectionPool
if not POSTGRES_CONNECTION_STRING:
raise ValueError("POSTGRES_CONNECTION_STRING not set")
self._async_conn_pool = AsyncConnectionPool(
POSTGRES_CONNECTION_STRING,
open=False
)
await self._async_conn_pool.open()
self._async_checkpointer = AsyncPostgresSaver(self._async_conn_pool)
# 检查表是否存在
tables_exist = await verify_checkpointer_tables_exist()
if not tables_exist:
# 尝试使用 setup() 方法
try:
await self._async_checkpointer.setup()
print(f"[Checkpointer] AsyncPostgresSaver.setup() executed")
except Exception as setup_e:
print(f"[Checkpointer] AsyncPostgresSaver.setup() failed: {setup_e}")
# 再次验证表是否被创建
tables_exist_after = await verify_checkpointer_tables_exist()
if not tables_exist_after:
print(f"[Checkpointer] Tables not created by setup(), using direct SQL...")
await create_checkpointer_tables_directly()
else:
print(f"[Checkpointer] Database tables already exist")
print(f"[Checkpointer] AsyncPostgresSaver created, connection: {POSTGRES_CONNECTION_STRING.split('@')[1] if '@' in POSTGRES_CONNECTION_STRING else POSTGRES_CONNECTION_STRING}")
except ImportError as e:
print(f"[WARNING] psycopg_pool or langgraph.checkpoint.postgres not installed: {e}, fallback to memory")
self._async_checkpointer = MemorySaver()
except Exception as e:
print(f"[Checkpointer] ERROR: Failed to create AsyncPostgresSaver: {e}")
raise
elif CHECKPOINTER_TYPE == "memory":
self._async_checkpointer = MemorySaver()
print("[Checkpointer] Using MemorySaver")
else:
self._async_checkpointer = MemorySaver()
print(f"[Checkpointer] Unknown type {CHECKPOINTER_TYPE}, using MemorySaver")
print(f"[Checkpointer] Async checkpointer type: {type(self._async_checkpointer).__name__}")
return self._async_checkpointer
async def setup(self):
"""
初始化 checkpointer创建数据库表等
"""
if not self._initialized:
await init_checkpointer_db()
try:
from tools.fault_record_db import init_fault_records_table
await init_fault_records_table()
except Exception as e:
print(f"[Checkpointer] 初始化 fault_records 表失败: {e}")
self._initialized = True
def reset(self):
"""重置 checkpointer主要用于测试"""
self._sync_checkpointer = None
self._async_checkpointer = None
self._async_conn_pool = None
self._initialized = False
checkpointer_manager = CheckpointerManager()
if __name__ == "__main__":
print("Checkpointer 配置测试:")
print(f" 类型: {CHECKPOINTER_TYPE}")
print(f" PostgreSQL 连接: {'已配置' if POSTGRES_CONNECTION_STRING else '未配置'}")

0
config Normal file
View File

View File

@ -12,10 +12,11 @@ load_dotenv()
# ==================== 大模型配置 ==================== # ==================== 大模型配置 ====================
LLM_CONFIG = { LLM_CONFIG = {
# "model": "Qwen3.5-35B-A3B", "model": "46-qwen3.5-35B",
"model": "Qwen3-32B", # "model": "Qwen3-32B",
# "model": "Qwen3-14B", # "model": "Qwen3-14B",
"base_url": "http://192.168.0.164:9800/v1", #"base_url": "http://192.168.1.164:9800/v1",
"base_url":"https://openai.zkzdht.com/v1",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5", "api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"temperature": float("0.1"), "temperature": float("0.1"),
"max_tokens": int("20096"), "max_tokens": int("20096"),
@ -37,7 +38,7 @@ _request_user_config: ContextVar[Optional[Dict[str, str]]] = ContextVar('request
# RAG配置的默认值 # RAG配置的默认值
_RAG_CONFIG_DEFAULT = { _RAG_CONFIG_DEFAULT = {
"search_endpoint": "http://htknow:8080/api/v1/knowledge/search/", "search_endpoint": "http://192.168.1.108:11000/api/v1/knowledge/search/",
"x-user-id": "1", "x-user-id": "1",
"x-user-name": "testuser", "x-user-name": "testuser",
"x-role": "admin", "x-role": "admin",
@ -155,7 +156,7 @@ SERVER_CONFIG = {
"host": "agent", "host": "agent",
"port": "9088", "port": "9088",
"base_url": "http://agent:9088", "base_url": "http://agent:9088",
"report" : "http://192.168.0.164:9088" "report" : "http://192.168.1.164:9088"
} }
# ==================== ASR服务配置 ==================== # ==================== ASR服务配置 ====================
@ -203,7 +204,7 @@ INDEX_CONFIG = {
"FULLTEXT_PROPERTY" :"fulltext", "FULLTEXT_PROPERTY" :"fulltext",
"EMBEDDING_PROPERTY" : "embedding", "EMBEDDING_PROPERTY" : "embedding",
"SEARCH_LABEL" : "Searchable", "SEARCH_LABEL" : "Searchable",
"VECTOR_INDEX_NAME" : "global_searchable_embedding", "VECTOR_INDEX_NAME" : "global_searchabl_embedding",
"FULLTEXT_INDEX_NAME" : "global_searchable_content_search" "FULLTEXT_INDEX_NAME" : "global_searchable_content_search"
} }

223
config.txt Normal file
View File

@ -0,0 +1,223 @@
"""
项目配置文件
从.env文件读取配置
"""
import os
from dotenv import load_dotenv
from contextvars import ContextVar
from typing import Optional, Dict, Any
# 加载.env文件
load_dotenv()
# ==================== 大模型配置 ====================
LLM_CONFIG = {
# "model": "Qwen3.5-35B-A3B",
"model": "Qwen3-32B",
# "model": "Qwen3-14B",
"base_url": "http://llm:9800/v1",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"temperature": float("0.1"),
"max_tokens": int("4096"),
"timeout": int("30")
}
# ==================== 嵌入模型配置 ====================
EMBEDDING_CONFIG = {
"model": "bge-m3",
"base_url": "http://embedding:9700/v1/embeddings",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"base_url_v2" : "http://embedding:9700/v1",
}
# ==================== 线程安全的请求上下文配置 ====================
# 使用 contextvars 存储每个请求的用户配置,确保多用户并发时不会相互干扰
_request_user_config: ContextVar[Optional[Dict[str, str]]] = ContextVar('request_user_config', default=None)
# RAG配置的默认值
_RAG_CONFIG_DEFAULT = {
"search_endpoint": "http://htknow:8080/api/v1/knowledge/search/",
"x-user-id": "1",
"x-user-name": "testuser",
"x-role": "admin",
"kb-id": None,
"file-id": None
}
class DynamicRAGConfig(dict):
"""
动态RAG配置类支持线程安全的请求级配置
当访问用户相关配置时,自动从当前请求上下文读取
其他配置项直接返回默认值
兼容字典的所有操作方式(如 get(), [], in 等)
"""
def __getitem__(self, key: str):
# 如果是用户相关配置或知识库参数,从当前请求上下文读取
if key in ["x-user-id", "x-user-name", "x-role", "kb-id", "file-id"]:
request_config = _request_user_config.get()
if request_config and key in request_config:
return request_config[key]
# 如果没有设置,返回默认值
return _RAG_CONFIG_DEFAULT[key]
# 其他配置项直接返回默认值
return _RAG_CONFIG_DEFAULT.get(key)
def get(self, key: str, default=None):
"""支持 get() 方法"""
try:
return self[key]
except KeyError:
return default
def __contains__(self, key: str):
"""支持 in 操作符"""
return key in _RAG_CONFIG_DEFAULT
def keys(self):
"""返回所有键"""
return _RAG_CONFIG_DEFAULT.keys()
def values(self):
"""返回所有值(动态读取用户配置)"""
return [self[key] for key in _RAG_CONFIG_DEFAULT.keys()]
def items(self):
"""返回所有键值对(动态读取用户配置)"""
return [(key, self[key]) for key in _RAG_CONFIG_DEFAULT.keys()]
# ==================== RAG服务配置 ====================
# 使用动态配置类,支持多用户并发时自动读取各自的配置
RAG_CONFIG = DynamicRAGConfig()
def set_request_user_config(x_user_id: Optional[str] = None,
x_user_name: Optional[str] = None,
x_role: Optional[str] = None,
text_kb_id: Optional[str] = None,
text_file_id: Optional[str] = None):
"""
设置当前请求的用户配置和知识库参数(线程安全)
Args:
x_user_id: 用户ID
x_user_name: 用户名称
x_role: 角色
text_kb_id: 文本检索知识库ID
text_file_id: 文本检索文件ID
"""
config = {}
if x_user_id is not None:
config["x-user-id"] = x_user_id
if x_user_name is not None:
config["x-user-name"] = x_user_name
if x_role is not None:
config["x-role"] = x_role
# 仅当非空字符串时才设置,空字符串时不传入 RAG 接口
if text_kb_id is not None and str(text_kb_id).strip():
config["kb-id"] = str(text_kb_id).strip()
if text_file_id is not None and str(text_file_id).strip():
config["file-id"] = str(text_file_id).strip()
_request_user_config.set(config if config else None)
# ==================== 图谱检索服务配置 ====================
GRAPH_SEARCH_CONFIG = {
"search_endpoint": "http://kgrag:9085/search_graph",
"graph_timeout": float("30.0"),
"default_top_k": int("10"),
"operation_search_endpoint": "http://kgrag:9085/operation_graph_search"
}
# ==================== 设备到系统检索服务配置 ====================
DEVICE_SYSTEM_CONFIG = {
"endpoint": "http://kgrag:9085/device_to_system",
"timeout": float("30.0"),
"default_min_hops": int("2"),
"default_max_hops": int("8"),
"default_top_k": int("10")
}
# ==================== 图册检索服务配置 ====================
ATLAS_CONFIG = {
"endpoint": "http://kgrag:9085/atlas_retrieval",
"timeout": float("30.0"),
"default_top_k": int("10")
}
# ==================== 工作流配置 ====================
WORKFLOW_CONFIG = {
"max_retry_count": int("2"), # 最大重试次数
"image_clarity_threshold": float("0.7"), # 图片清晰度阈值
"default_timeout": int("30") # 默认超时时间(秒)
}
# ==================== 服务器配置 ====================
SERVER_CONFIG = {
"host": "agent",
"port": "59088",
"base_url": "http://agent:9088"
}
# ==================== ASR服务配置 ====================
ASR_CONFIG = {
"api_base": "http://asr:9900/v1",
"model": "base",
"language": "zh",
2"timeout": int("30")
}
# ==================== VLM服务配置 ====================
VLM_CONFIG = {
"api_base" : "http://kgrag:9085/v1",
"model" : "Qwen3-VL-8B"
}
# ==================== Neo4J配置 ====================
NEO4J_CONFIG = {
"uri": "neo4j://neo4j:7687",
"user": "neo4j",
"password": "zdht123@"
}
# ==================== 知识库匹配配置 ====================
KB_TREE_CONFIG = {
"url": "http://htknow:8080/api/v1/knowledge/knowledge_base/tree"
}
# ==================== 维修反馈统计配置 ====================
REPAIR_FEEDBACK_CONFIG = {
"url": "http://webui:22000",
"email": "15088888888@163.com",
"password": "11223344"
}
# ==================== 数据库配置 ====================
POSTGRES_CONNECTION_STRING = "postgresql://postgres:0737d36c55d3614fc0b36d69e75fcea97283cdcebda7fe25@postgres:5432/hj_webui"
#POSTGRES_CONNECTION_STRING = "postgresql+psycopg://postgres:password@postgres:5432/an_webui"
POSTGRES_SQLALCHEMY_URL = "postgresql+psycopg://postgres:password@postgres:5432/an_webui"
# ==================== 索引配置 ====================
INDEX_CONFIG = {
"NAME_PROPERTY" : "名称",
"FULLTEXT_PROPERTY" :"fulltext",
"EMBEDDING_PROPERTY" : "embedding",
"SEARCH_LABEL" : "Searchable",
"VECTOR_INDEX_NAME" : "searchable_vector",
"FULLTEXT_INDEX_NAME" : "global_searchable_content_search"
}
# ==================== sql表格字段与neo4j节点映射 ====================
MAP_INS={
"故障模式" : "fault",
"系统" : "system_name",
"设备" : "device_name",
"舷号" : "ship_number",
}
# ==================== neo4j配置 ====================
NEO4J_CONFIG = {
"uri": "neo4j://neo4j:7687",
"user": "neo4j",
"password": "zdht123@"
}

221
config521.py Normal file
View File

@ -0,0 +1,221 @@
"""
项目配置文件
.env文件读取配置
"""
import os
from dotenv import load_dotenv
from contextvars import ContextVar
from typing import Optional, Dict, Any
# 加载.env文件
load_dotenv()
# ==================== 大模型配置 ====================
LLM_CONFIG = {
"model": "Qwen3.5-35B-A3B",
# "model": "Qwen3-32B",
"base_url": "http://192.168.0.46:59800/v1",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"temperature": float("0.1"),
"max_tokens": int("4096"),
"timeout": int("30")
}
# ==================== 嵌入模型配置 ====================
EMBEDDING_CONFIG = {
"model": "bge-m3",
"base_url": "http://192.168.0.46:59700/v1/embeddings",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
}
# ==================== 线程安全的请求上下文配置 ====================
# 使用 contextvars 存储每个请求的用户配置,确保多用户并发时不会相互干扰
_request_user_config: ContextVar[Optional[Dict[str, str]]] = ContextVar('request_user_config', default=None)
# RAG配置的默认值
_RAG_CONFIG_DEFAULT = {
"search_endpoint": "http://192.168.0.46:11000/api/v1/knowledge/search/",
"x-user-id": "1",
"x-user-name": "testuser",
"x-role": "admin",
"kb-id": None,
"file-id": None
}
class DynamicRAGConfig(dict):
"""
动态RAG配置类支持线程安全的请求级配置
当访问用户相关配置时自动从当前请求上下文读取
其他配置项直接返回默认值
兼容字典的所有操作方式 get(), [], in
"""
def __getitem__(self, key: str):
# 如果是用户相关配置或知识库参数,从当前请求上下文读取
if key in ["x-user-id", "x-user-name", "x-role", "kb-id", "file-id"]:
request_config = _request_user_config.get()
if request_config and key in request_config:
return request_config[key]
# 如果没有设置,返回默认值
return _RAG_CONFIG_DEFAULT[key]
# 其他配置项直接返回默认值
return _RAG_CONFIG_DEFAULT.get(key)
def get(self, key: str, default=None):
"""支持 get() 方法"""
try:
return self[key]
except KeyError:
return default
def __contains__(self, key: str):
"""支持 in 操作符"""
return key in _RAG_CONFIG_DEFAULT
def keys(self):
"""返回所有键"""
return _RAG_CONFIG_DEFAULT.keys()
def values(self):
"""返回所有值(动态读取用户配置)"""
return [self[key] for key in _RAG_CONFIG_DEFAULT.keys()]
def items(self):
"""返回所有键值对(动态读取用户配置)"""
return [(key, self[key]) for key in _RAG_CONFIG_DEFAULT.keys()]
# ==================== RAG服务配置 ====================
# 使用动态配置类,支持多用户并发时自动读取各自的配置
RAG_CONFIG = DynamicRAGConfig()
def set_request_user_config(x_user_id: Optional[str] = None,
x_user_name: Optional[str] = None,
x_role: Optional[str] = None,
text_kb_id: Optional[str] = None,
text_file_id: Optional[str] = None):
"""
设置当前请求的用户配置和知识库参数线程安全
Args:
x_user_id: 用户ID
x_user_name: 用户名称
x_role: 角色
text_kb_id: 文本检索知识库ID
text_file_id: 文本检索文件ID
"""
config = {}
if x_user_id is not None:
config["x-user-id"] = x_user_id
if x_user_name is not None:
config["x-user-name"] = x_user_name
if x_role is not None:
config["x-role"] = x_role
# 仅当非空字符串时才设置,空字符串时不传入 RAG 接口
if text_kb_id is not None and str(text_kb_id).strip():
config["kb-id"] = str(text_kb_id).strip()
if text_file_id is not None and str(text_file_id).strip():
config["file-id"] = str(text_file_id).strip()
_request_user_config.set(config if config else None)
# ==================== 图谱检索服务配置 ====================
GRAPH_SEARCH_CONFIG = {
"search_endpoint": "http://192.168.0.46:59085/search_graph",
"graph_timeout": float("30.0"),
"default_top_k": int("10"),
"operation_search_endpoint": "http://192.168.0.46:59085/operation_graph_search"
}
# ==================== 设备到系统检索服务配置 ====================
DEVICE_SYSTEM_CONFIG = {
"endpoint": "http://192.168.0.46:59085/device_to_system",
"timeout": float("30.0"),
"default_min_hops": int("2"),
"default_max_hops": int("8"),
"default_top_k": int("10")
}
# ==================== 图册检索服务配置 ====================
ATLAS_CONFIG = {
"endpoint": "http://192.168.0.46:59085/atlas_retrieval",
"timeout": float("30.0"),
"default_top_k": int("10")
}
# ==================== 工作流配置 ====================
WORKFLOW_CONFIG = {
"max_retry_count": int("2"), # 最大重试次数
"image_clarity_threshold": float("0.7"), # 图片清晰度阈值
"default_timeout": int("30") # 默认超时时间(秒)
}
# ==================== 服务器配置 ====================
SERVER_CONFIG = {
"host": "192.168.0.46",
"port": "59088",
"base_url": "http://192.168.0.46:59088"
}
# ==================== ASR服务配置 ====================
ASR_CONFIG = {
"api_base": "http://192.168.0.46:59900/v1",
"model": "base",
"language": "zh",
"timeout": int("30")
}
# ==================== VLM服务配置 ====================
VLM_CONFIG = {
"api_base" : "http://192.168.0.46:59801/v1",
"model" : "Qwen3-VL-8B"
}
# ==================== 任务类型映射 ====================
TASK_TYPE_MAPPING = {
"fault_diagnosis": "故障诊断",
"repair": "维修",
"statistics": "统计",
"qa": "常规问答",
"report": "报告生成"
}
# ==================== Neo4J配置 ====================
NEO4J_CONFIG = {
"uri": "neo4j://192.168.0.46:57687",
"user": "neo4j",
"password": "zdht123@"
}
# ==================== 知识库匹配配置 ====================
KB_TREE_CONFIG = {
"url": "http://192.168.0.46:11000/api/v1/knowledge/knowledge_base/tree"
}
# ==================== 维修反馈统计配置 ====================
REPAIR_FEEDBACK_CONFIG = {
"url": "http://192.168.0.46:22000",
"email": "15088888888@163.com",
"password": "11223344"
}
# ==================== 舷号-型号映射配置 ====================
# 格式:{型号名称: {"numbers": [舷号列表], "aliases": [别名列表], "names": [舰名列表]}}
SHIP_MODEL_MAPPING = {
"055型驱逐舰": {
"numbers": ["102", "103", "104", "105", "106", "107", "108"],
"aliases": ["055", "055型", "055驱逐舰", "万吨大驱"],
"names": ["拉萨舰", "鞍山舰", "无锡舰", "大连舰", "延安舰", "遵义舰", "咸阳舰"],
},
"052D型驱逐舰": {
"numbers": ["172", "173", "174", "175", "163"],
"aliases": ["052D", "052D型", "052D驱逐舰"],
"names": ["昆明舰", "长沙舰", "合肥舰", "银川舰", "南昌舰"],
},
"054A型护卫舰": {
"numbers": ["529", "530", "547", "568", "570"],
"aliases": ["054A", "054A型", "054A护卫舰"],
"names": ["舟山舰", "徐州舰", "临沂舰", "衡阳舰", "黄山舰"],
},
}
# ==================== 数据库配置 ====================
POSTGRES_CONNECTION_STRING = "postgresql://postgres:password@192.168.0.46:5432/an_webui"

BIN
docker-images/wx_agent_latest.docker-save.tar.gz (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -176,10 +176,10 @@ def route_to_workflow(state: MainAgentState) -> Dict[str, Any]:
print(f"[主脑Agent] 路由到: {route_flag}") print(f"[主脑Agent] 路由到: {route_flag}")
print(f"[主脑Agent] 参数: combined_query={combined_query[:100]}...") print(f"[主脑Agent] 参数: combined_query={combined_query[:100]}...")
if route_flag == "故障排查与修理": if route_flag == "问题排查":
detail = "正在调用故障排查与修理智能体" detail = "正在调用问题排查智能体"
elif route_flag == "舰船百科": elif route_flag == "百科问答":
detail = "正在调用舰船百科智能体" detail = "正在调用百科问答智能体"
elif route_flag == "操作使用": elif route_flag == "操作使用":
detail = "正在调用操作使用智能体" detail = "正在调用操作使用智能体"
elif route_flag == "统计": elif route_flag == "统计":

285
main_agent63.py Normal file
View File

@ -0,0 +1,285 @@
"""
主脑Agent - 简化版无意图分类
职责
1. 输入处理图片音频文本提取
2. 直接路由根据外部传入的 route_flag 决定调用哪个工作流
工作流列表来自 workflow_registry
- 故障排查与修理
- 舰船百科
- 操作使用
"""
from typing import TypedDict, Literal, Optional, Dict, Any, List
from langgraph.graph import StateGraph, START, END
from tools.function_tool import (
vlm_image_to_text,
asr_audio_to_text,
detect_input_type
)
from modelsAPI.model_api import OpenaiAPI
from utils.function_tracker import track_function_calls
from workflow_registry import VALID_ROUTE_FLAGS
import json
import re
class MainAgentState(TypedDict):
"""主脑Agent状态"""
raw_input: Dict[str, Any]
has_image: bool
has_audio: bool
has_query: bool
has_file_text: bool
input_type: str
history_message: str
extracted_text: str
image_description: str
asr_text: str
file_text: str
combined_query: str
workflow_result: Optional[Dict[str, Any]]
final_response: str
error_message: str
route_flag: str
route_params: Dict[str, Any]
def detect_input_type_node(state: MainAgentState) -> Dict[str, Any]:
"""检测输入类型"""
raw = state["raw_input"]
result = detect_input_type.invoke({"raw_input": raw})
has_image = result.get("has_image", False)
has_audio = result.get("has_audio", False)
has_query = result.get("has_query", False)
has_file_text = False
file_text_v = raw.get("file_text")
if isinstance(file_text_v, str) and file_text_v.strip():
has_file_text = True
input_type = "mixed"
count = sum([has_image, has_audio, has_query, has_file_text])
if count == 1:
if has_image:
input_type = "image"
elif has_audio:
input_type = "audio"
elif has_query:
input_type = "text"
elif has_file_text:
input_type = "file"
return {
"has_image": has_image,
"has_audio": has_audio,
"has_query": has_query,
"has_file_text": has_file_text,
"input_type": input_type,
}
async def process_multimodal_input(state: MainAgentState) -> Dict[str, Any]:
"""处理多模态输入"""
has_image = state.get("has_image", False)
has_audio = state.get("has_audio", False)
has_query = state.get("has_query", False)
has_file_text = state.get("has_file_text", False)
raw = state["raw_input"]
image_description = ""
asr_text = ""
extracted_text = ""
try:
if has_image:
image_path = raw.get("image", "")
if image_path:
try:
image_description = await vlm_image_to_text.ainvoke({
"image_path": image_path
})
if not image_description or image_description.startswith("VLM 调用失败"):
image_description = ""
except Exception:
image_description = ""
if has_audio:
audio_path = raw.get("audio", "")
if audio_path:
try:
asr_text = await asr_audio_to_text.ainvoke({
"audio_path": audio_path
})
if not asr_text or asr_text.startswith("ASR 调用失败"):
asr_text = ""
except Exception:
asr_text = ""
text_parts = []
if raw.get("query"):
text_parts.append(raw["query"])
if image_description:
text_parts.append(f"[图片描述] {image_description}")
if asr_text:
text_parts.append(f"[语音转文字] {asr_text}")
if raw.get("file_text"):
text_parts.append(f"[文件内容] {raw['file_text']}")
extracted_text = "\n".join(text_parts) if text_parts else ""
combined_query = raw.get("query", "") or extracted_text
return {
"extracted_text": extracted_text,
"image_description": image_description,
"asr_text": asr_text,
"combined_query": combined_query,
"file_text": raw.get("file_text", ""),
}
except Exception as e:
return {
"error_message": f"输入处理失败: {str(e)}",
"extracted_text": "",
}
# @track_function_calls
def route_to_workflow(state: MainAgentState) -> Dict[str, Any]:
"""
🤖分析意图调用相关智能体
"""
route_flag = state.get("route_flag", "故障排查与修理")
if route_flag not in VALID_ROUTE_FLAGS:
route_flag = "故障排查与修理"
query_text = state.get("extracted_text", "")
combined_query = state.get("combined_query", "") or query_text
raw_input = state.get("raw_input", {})
history_message = raw_input.get("history_message", "")
route_params = {
"extracted_text": query_text,
"combined_query": combined_query,
"history_message": history_message,
}
if raw_input.get("file_text"):
route_params["file_text"] = raw_input["file_text"]
print(f"[主脑Agent] 路由到: {route_flag}")
print(f"[主脑Agent] 参数: combined_query={combined_query[:100]}...")
if route_flag == "故障排查与修理":
detail = "正在调用故障排查与修理智能体"
elif route_flag == "舰船百科":
detail = "正在调用舰船百科智能体"
elif route_flag == "操作使用":
detail = "正在调用操作使用智能体"
elif route_flag == "统计":
detail = "正在调用统计智能体"
else:
detail = "正在调用通用智能体"
return {
"route_flag": route_flag,
"route_params": route_params,
"__detail__": detail,
}
def create_main_agent():
"""创建主脑Agent"""
workflow = StateGraph(MainAgentState)
workflow.add_node("detect_input_type", detect_input_type_node)
workflow.add_node("process_input", process_multimodal_input)
workflow.add_node("route", route_to_workflow)
workflow.add_edge(START, "detect_input_type")
workflow.add_edge("detect_input_type", "process_input")
workflow.add_edge("process_input", "route")
workflow.add_edge("route", END)
return workflow.compile()
def filter_image_urls(history_message: str) -> Any:
"""过滤历史消息中的图片URL"""
if not history_message:
return history_message
try:
history_messages = json.loads(history_message) if isinstance(history_message, str) else history_message
except (json.JSONDecodeError, TypeError):
return history_message
if not isinstance(history_messages, list):
return history_messages
filtered_messages = []
for msg in history_messages:
if not isinstance(msg, dict):
filtered_messages.append(msg)
continue
new_msg = msg.copy()
content = msg.get("content")
if isinstance(content, list):
text_parts = []
for content_item in content:
if isinstance(content_item, dict):
if content_item.get("type") == "text":
text_value = content_item.get("text", "")
if text_value:
text_parts.append(text_value)
elif isinstance(content_item, str):
text_parts.append(content_item)
if text_parts:
new_msg["content"] = "".join(text_parts)
filtered_messages.append(new_msg)
else:
filtered_messages.append(new_msg)
return filtered_messages
async def extract_conversation_title(content: str) -> str:
"""提取对话标题"""
try:
prompt = f"请从以下对话内容中提取一个简短的标题不超过20字\n\n{content[:500]}"
response = await OpenaiAPI.open_api_chat_without_thinking(
query=prompt,
model=None,
system_prompt="你是一个标题提取助手,请从对话内容中提取一个简短的标题。",
messages=[]
)
title = response.strip() if response else "新对话"
if len(title) > 30:
title = title[:30] + "..."
return title
except Exception:
return "新对话"
if __name__ == "__main__":
import asyncio
async def test():
print("测试主脑Agent")
agent = create_main_agent()
result = await agent.ainvoke({
"raw_input": {"query": "101舰主机发生异响"},
"route_flag": "故障排查与修理"
})
print(f"结果: {result}")
asyncio.run(test())

304
modelsAPI/model_api420.py Normal file
View File

@ -0,0 +1,304 @@
import asyncio
import httpx
from typing import List, Union
import os
import numpy as np
from openai import OpenAI
from config import LLM_CONFIG, EMBEDDING_CONFIG
from openai import AsyncOpenAI
class OpenaiAPI:
@staticmethod
async def open_api_chat_without_thinking(
query: str = None,
model: str = None,
json_output: bool = False,
system_prompt: str = None,
messages: list = None,
enable_thinking: bool = True,
temperature: float = 0.1
) -> str:
if model is None:
model = LLM_CONFIG["model"]
if system_prompt is None:
system_prompt = "我是一个专业的AI助手请用中文回答问题。请直接给出最终答案不要包含任何推理步骤、思考过程、解释性文字或前缀。"
# 构建消息
message_list = [{"role": "system", "content": system_prompt}]
if messages is not None:
message_list.extend(messages)
if query is not None:
message_list.append({"role": "user", "content": query})
client = AsyncOpenAI(
api_key=LLM_CONFIG["api_key"],
base_url=LLM_CONFIG["base_url"],
)
kwargs = {
"model": model,
"messages": message_list,
"temperature": temperature,
"stream": False,
"max_tokens":LLM_CONFIG["max_tokens"]
}
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
if json_output:
kwargs["response_format"] = {"type": "json_object"}
print("model API. history message", message_list)
response = await client.chat.completions.create(**kwargs)
return response.choices[0].message.content
@staticmethod
async def open_api_vl_without_thinking(
image_url: str
) -> str:
"""
专门用于从图片中提取设备名称故障现象
自动过滤思考过程仅返回核心结果
参数:
image_url: Base64 格式的图片数据 (data:image/...;base64,...)
model: 模型名称默认为配置中的模型
custom_instruction: 额外的特定指令 (可选)
"""
# 1. 确定模型名称
model = LLM_CONFIG.get("model")
# 2. 构建强约束的 System Prompt
# 核心目标:禁止思考标签,禁止废话,只给结果
system_prompt = (
"你是一个工业视觉分析专家。你的任务是从图片中识别设备并诊断故障。\n"
"【严格约束】\n"
"1. 绝对禁止输出 <think>, <think>, <reasoning> 等任何思考过程标签。\n"
"2. 绝对禁止输出'好的''根据图片''分析如下'等开场白或结束语。\n"
"3. 直接输出最终结论,格式必须严格遵守下面的模板。"
)
# 3. 构建针对性的 User Prompt
default_task = (
"请分析这张图片,描述图片内容,重点关注以下信息\n"
"1. 设备识别:图中是什么设备?型号或标签是否可见?\n"
"2. 状态评估:仪表盘数值是否在正常区间?指示灯颜色是什么?\n"
"3. 异常检测:是否存在漏油、生锈、电缆破损、螺丝松动或烟雾?\n"
"4. 环境风险:周围是否有杂物堆放或安全隐患?\n"
"请用专业、客观、简短的语言描述。"
)
final_user_prompt = default_task
# 4. 初始化客户端
client = AsyncOpenAI(
api_key=LLM_CONFIG.get("api_key"),
base_url=LLM_CONFIG.get("base_url"),
)
# 5. 构建请求参数
kwargs = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": [
{"type": "text", "text": final_user_prompt},
{"type": "image_url", "image_url": {"url": image_url}}
]
}
],
"temperature": 0.1, # 低温度以保证事实准确性
"stream": False,
"max_tokens": LLM_CONFIG.get("max_tokens"),
}
# 尝试通过参数关闭思考 (取决于后端支持情况)
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
try:
print(f"🚀 正在调用 {model} 进行设备故障分析...")
response = await client.chat.completions.create(**kwargs)
raw_content = response.choices[0].message.content or ""
print(f"✅ 分析完成:\n{raw_content}")
return raw_content
except Exception as e:
import traceback
error_msg = f"❌ 设备故障分析失败:{str(e)}\n{traceback.format_exc()}"
print(error_msg)
return f"Error: {str(e)}"
@staticmethod
def get_embeddings(embedding_text: str):
embedding_base_url = EMBEDDING_CONFIG["base_url"]
with httpx.Client(timeout=60) as client:
response = client.post(
embedding_base_url,
json={"model": EMBEDDING_CONFIG["model"], "input": embedding_text},
headers={"Authorization": EMBEDDING_CONFIG["api_key"]},
)
embedding_text = response.json()["data"][0]["embedding"]
return embedding_text
@staticmethod
async def get_embeddings_async(embedding_text: str) -> List[float]:
embedding_base_url = EMBEDDING_CONFIG["base_url"]
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(
embedding_base_url,
json={"model": EMBEDDING_CONFIG["model"], "input": embedding_text},
headers={"Authorization": EMBEDDING_CONFIG["api_key"]},
)
embedding = response.json()["data"][0]["embedding"]
return embedding
@staticmethod
async def get_embeddings_batch_async(texts: List[str]) -> List[List[float]]:
embedding_base_url = EMBEDDING_CONFIG["base_url"]
async with httpx.AsyncClient(timeout=120) as client:
response = await client.post(
embedding_base_url,
json={"model": EMBEDDING_CONFIG["model"], "input": texts},
headers={"Authorization": EMBEDDING_CONFIG["api_key"]},
)
data = response.json()["data"]
sorted_data = sorted(data, key=lambda x: x["index"])
embeddings = [item["embedding"] for item in sorted_data]
return embeddings
@staticmethod
def cosine_similarity(vec1: Union[List[float], np.ndarray], vec2: Union[List[float], np.ndarray]) -> float:
v1 = np.array(vec1) if not isinstance(vec1, np.ndarray) else vec1
v2 = np.array(vec2) if not isinstance(vec2, np.ndarray) else vec2
norm1 = np.linalg.norm(v1)
norm2 = np.linalg.norm(v2)
if norm1 == 0 or norm2 == 0:
return 0.0
return float(np.dot(v1, v2) / (norm1 * norm2))
@staticmethod
def cosine_similarity_batch(query_vec: Union[List[float], np.ndarray],
candidates_vecs: List[Union[List[float], np.ndarray]]) -> List[float]:
query = np.array(query_vec) if not isinstance(query_vec, np.ndarray) else query_vec
candidates = [np.array(v) if not isinstance(v, np.ndarray) else v for v in candidates_vecs]
query_norm = np.linalg.norm(query)
if query_norm == 0:
return [0.0] * len(candidates)
similarities = []
for candidate in candidates:
cand_norm = np.linalg.norm(candidate)
if cand_norm == 0:
similarities.append(0.0)
else:
sim = float(np.dot(query, candidate) / (query_norm * cand_norm))
similarities.append(sim)
return similarities
@staticmethod
async def hybrid_match_with_embeddings(
query_text: str,
candidates: List[dict],
name_key: str = "display_name",
embedding_key: str = "embedding",
text_weight: float = 0.3,
semantic_weight: float = 0.7,
text_threshold: float = 0.3,
semantic_threshold: float = 0.5
) -> tuple:
if not candidates:
return None, 0.0
query_embedding = await OpenaiAPI.get_embeddings_async(query_text)
scored_candidates = []
for idx, candidate in enumerate(candidates):
name = candidate.get(name_key, "")
if not name:
props = candidate.get("props", {})
name = props.get("name") or props.get("名称") or props.get("设备名称") or ""
text_score = OpenaiAPI._compute_text_similarity(query_text, name)
props = candidate.get("props", {})
candidate_embedding = props.get(embedding_key) or candidate.get(embedding_key)
if candidate_embedding:
semantic_score = OpenaiAPI.cosine_similarity(query_embedding, candidate_embedding)
else:
semantic_score = 0.0
combined_score = text_weight * text_score + semantic_weight * semantic_score
scored_candidates.append({
"candidate": candidate,
"text_score": text_score,
"semantic_score": semantic_score,
"combined_score": combined_score,
"index": idx
})
scored_candidates.sort(key=lambda x: x["combined_score"], reverse=True)
best = scored_candidates[0]
best_candidate = best["candidate"]
best_score = best["combined_score"]
min_threshold = max(text_threshold * text_weight + semantic_threshold * semantic_weight, 0.3)
if best_score < min_threshold:
return None, best_score
return best_candidate, best_score
@staticmethod
def _compute_text_similarity(a: str, b: str) -> float:
if not a or not b:
return 0.0
a = str(a).strip().lower()
b = str(b).strip().lower()
if not a or not b:
return 0.0
if a == b:
return 1.0
if a in b or b in a:
return 0.9
set_a, set_b = set(a), set(b)
inter = len(set_a & set_b)
union = len(set_a | set_b) or 1
jaccard = inter / union
len_diff = abs(len(a) - len(b))
len_penalty = max(0.0, 1.0 - len_diff / max(len(a), len(b), 1))
return 0.5 * jaccard + 0.5 * len_penalty
if __name__ == "__main__":
answer = asyncio.run(OpenaiAPI.open_api_chat_without_thinking(
query="你好",
json_output=True,
system_prompt="你是一个助手,请用中文回答问题。请直接给出最终答案,不要包含任何推理步骤、思考过程、解释性文字或前缀。",
enable_thinking=True
))
print(answer)

380
modelsAPI/model_api64.py Normal file
View File

@ -0,0 +1,380 @@
import asyncio
import httpx
from typing import List, Union
import os
import numpy as np
from openai import OpenAI
from config import LLM_CONFIG, EMBEDDING_CONFIG
from neo4j_graphrag.embeddings.base import Embedder
from openai import AsyncOpenAI
class OpenaiAPI:
@staticmethod
async def open_api_chat_without_thinking(
query: str = None,
model: str = None,
json_output: bool = False,
system_prompt: str = None,
messages: list = None,
enable_thinking: bool = True,
temperature: float = 0.1
) -> str:
if model is None:
model = LLM_CONFIG["model"]
if system_prompt is None:
system_prompt = "我是一个专业的AI助手请用中文回答问题。请直接给出最终答案不要包含任何推理步骤、思考过程、解释性文字或前缀。"
# 构建消息
message_list = [{"role": "system", "content": system_prompt}]
if messages is not None:
message_list.extend(messages)
if query is not None:
message_list.append({"role": "user", "content": query})
client = AsyncOpenAI(
api_key=LLM_CONFIG["api_key"],
base_url=LLM_CONFIG["base_url"],
)
kwargs = {
"model": model,
"messages": message_list,
"temperature": temperature,
"stream": False,
"max_tokens":LLM_CONFIG["max_tokens"]
}
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
if json_output:
kwargs["response_format"] = {"type": "json_object"}
print("model API. history message", message_list)
response = await client.chat.completions.create(**kwargs)
return response.choices[0].message.content
@staticmethod
async def open_api_chat_stream(
query: str = None,
model: str = None,
json_output: bool = False,
system_prompt: str = None,
messages: list = None,
enable_thinking: bool = True,
temperature: float = 0.1
):
"""
流式输出大模型响应
Yields:
str: 流式内容片段
"""
if model is None:
model = LLM_CONFIG["model"]
if system_prompt is None:
system_prompt = "我是一个专业的AI助手请用中文回答问题。请直接给出最终答案不要包含任何推理步骤、思考过程、解释性文字或前缀。"
# 构建消息
message_list = [{"role": "system", "content": system_prompt}]
if messages is not None:
message_list.extend(messages)
if query is not None:
message_list.append({"role": "user", "content": query})
client = AsyncOpenAI(
api_key=LLM_CONFIG["api_key"],
base_url=LLM_CONFIG["base_url"],
)
kwargs = {
"model": model,
"messages": message_list,
"temperature": temperature,
"stream": True,
"max_tokens":LLM_CONFIG["max_tokens"]
}
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
if json_output:
kwargs["response_format"] = {"type": "json_object"}
print("model API stream. history message", message_list)
stream = await client.chat.completions.create(**kwargs)
async for chunk in stream:
if chunk.choices and len(chunk.choices) > 0:
delta = chunk.choices[0].delta
if delta.content:
yield delta.content
@staticmethod
async def open_api_vl_without_thinking(
image_url: str
) -> str:
"""
专门用于从图片中提取设备名称故障现象
自动过滤思考过程仅返回核心结果
参数:
image_url: Base64 格式的图片数据 (data:image/...;base64,...)
model: 模型名称默认为配置中的模型
custom_instruction: 额外的特定指令 (可选)
"""
# 1. 确定模型名称
model = LLM_CONFIG.get("model")
# 2. 构建强约束的 System Prompt
# 核心目标:禁止思考标签,禁止废话,只给结果
system_prompt = (
"你是一个工业视觉分析专家。你的任务是从图片中识别设备并诊断故障。\n"
"【严格约束】\n"
"1. 绝对禁止输出 <think>, <think>, <reasoning> 等任何思考过程标签。\n"
"2. 绝对禁止输出'好的''根据图片''分析如下'等开场白或结束语。\n"
"3. 直接输出最终结论,格式必须严格遵守下面的模板。"
)
# 3. 构建针对性的 User Prompt
default_task = (
"请分析这张图片,描述图片内容,重点关注以下信息\n"
"1. 设备识别:图中是什么设备?型号或标签是否可见?\n"
"2. 状态评估:仪表盘数值是否在正常区间?指示灯颜色是什么?\n"
"3. 异常检测:是否存在漏油、生锈、电缆破损、螺丝松动或烟雾?\n"
"4. 环境风险:周围是否有杂物堆放或安全隐患?\n"
"请用专业、客观、简短的语言描述。"
)
final_user_prompt = default_task
# 4. 初始化客户端
client = AsyncOpenAI(
api_key=LLM_CONFIG.get("api_key"),
base_url=LLM_CONFIG.get("base_url"),
)
# 5. 构建请求参数
kwargs = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": [
{"type": "text", "text": final_user_prompt},
{"type": "image_url", "image_url": {"url": image_url}}
]
}
],
"temperature": 0.1, # 低温度以保证事实准确性
"stream": False,
"max_tokens": LLM_CONFIG.get("max_tokens"),
}
# 尝试通过参数关闭思考 (取决于后端支持情况)
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
try:
print(f"🚀 正在调用 {model} 进行设备故障分析...")
response = await client.chat.completions.create(**kwargs)
raw_content = response.choices[0].message.content or ""
print(f"✅ 分析完成:\n{raw_content}")
return raw_content
except Exception as e:
import traceback
error_msg = f"❌ 设备故障分析失败:{str(e)}\n{traceback.format_exc()}"
print(error_msg)
return f"Error: {str(e)}"
@staticmethod
def get_embeddings(embedding_text: str):
embedding_base_url = EMBEDDING_CONFIG["base_url"]
with httpx.Client(timeout=60) as client:
response = client.post(
embedding_base_url,
json={"model": EMBEDDING_CONFIG["model"], "input": embedding_text},
headers={"Authorization": EMBEDDING_CONFIG["api_key"]},
)
embedding_text = response.json()["data"][0]["embedding"]
return embedding_text
@staticmethod
async def get_embeddings_async(embedding_text: str) -> List[float]:
embedding_base_url = EMBEDDING_CONFIG["base_url"]
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(
embedding_base_url,
json={"model": EMBEDDING_CONFIG["model"], "input": embedding_text},
headers={"Authorization": EMBEDDING_CONFIG["api_key"]},
)
embedding = response.json()["data"][0]["embedding"]
return embedding
@staticmethod
async def get_embeddings_batch_async(texts: List[str]) -> List[List[float]]:
embedding_base_url = EMBEDDING_CONFIG["base_url"]
async with httpx.AsyncClient(timeout=120) as client:
response = await client.post(
embedding_base_url,
json={"model": EMBEDDING_CONFIG["model"], "input": texts},
headers={"Authorization": EMBEDDING_CONFIG["api_key"]},
)
data = response.json()["data"]
sorted_data = sorted(data, key=lambda x: x["index"])
embeddings = [item["embedding"] for item in sorted_data]
return embeddings
@staticmethod
def cosine_similarity(vec1: Union[List[float], np.ndarray], vec2: Union[List[float], np.ndarray]) -> float:
v1 = np.array(vec1) if not isinstance(vec1, np.ndarray) else vec1
v2 = np.array(vec2) if not isinstance(vec2, np.ndarray) else vec2
norm1 = np.linalg.norm(v1)
norm2 = np.linalg.norm(v2)
if norm1 == 0 or norm2 == 0:
return 0.0
return float(np.dot(v1, v2) / (norm1 * norm2))
@staticmethod
def cosine_similarity_batch(query_vec: Union[List[float], np.ndarray],
candidates_vecs: List[Union[List[float], np.ndarray]]) -> List[float]:
query = np.array(query_vec) if not isinstance(query_vec, np.ndarray) else query_vec
candidates = [np.array(v) if not isinstance(v, np.ndarray) else v for v in candidates_vecs]
query_norm = np.linalg.norm(query)
if query_norm == 0:
return [0.0] * len(candidates)
similarities = []
for candidate in candidates:
cand_norm = np.linalg.norm(candidate)
if cand_norm == 0:
similarities.append(0.0)
else:
sim = float(np.dot(query, candidate) / (query_norm * cand_norm))
similarities.append(sim)
return similarities
@staticmethod
async def hybrid_match_with_embeddings(
query_text: str,
candidates: List[dict],
name_key: str = "display_name",
embedding_key: str = "embedding",
text_weight: float = 0.3,
semantic_weight: float = 0.7,
text_threshold: float = 0.3,
semantic_threshold: float = 0.5
) -> tuple:
if not candidates:
return None, 0.0
query_embedding = await OpenaiAPI.get_embeddings_async(query_text)
scored_candidates = []
for idx, candidate in enumerate(candidates):
name = candidate.get(name_key, "")
if not name:
props = candidate.get("props", {})
name = props.get("name") or props.get("名称") or props.get("设备名称") or ""
text_score = OpenaiAPI._compute_text_similarity(query_text, name)
props = candidate.get("props", {})
candidate_embedding = props.get(embedding_key) or candidate.get(embedding_key)
if candidate_embedding:
semantic_score = OpenaiAPI.cosine_similarity(query_embedding, candidate_embedding)
else:
semantic_score = 0.0
combined_score = text_weight * text_score + semantic_weight * semantic_score
scored_candidates.append({
"candidate": candidate,
"text_score": text_score,
"semantic_score": semantic_score,
"combined_score": combined_score,
"index": idx
})
scored_candidates.sort(key=lambda x: x["combined_score"], reverse=True)
best = scored_candidates[0]
best_candidate = best["candidate"]
best_score = best["combined_score"]
min_threshold = max(text_threshold * text_weight + semantic_threshold * semantic_weight, 0.3)
if best_score < min_threshold:
return None, best_score
return best_candidate, best_score
@staticmethod
def _compute_text_similarity(a: str, b: str) -> float:
if not a or not b:
return 0.0
a = str(a).strip().lower()
b = str(b).strip().lower()
if not a or not b:
return 0.0
if a == b:
return 1.0
if a in b or b in a:
return 0.9
set_a, set_b = set(a), set(b)
inter = len(set_a & set_b)
union = len(set_a | set_b) or 1
jaccard = inter / union
len_diff = abs(len(a) - len(b))
len_penalty = max(0.0, 1.0 - len_diff / max(len(a), len(b), 1))
return 0.5 * jaccard + 0.5 * len_penalty
class LocalBgeM3Embeddings(Embedder):
def __init__(self, base_url: str, api_key: str, model_name: str = "bge-m3"):
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.model_name = model_name
def embed_query(self, text: str) -> List[float]:
return self.embed_documents([text])[0]
def embed_documents(self, texts: List[str]) -> List[List[float]]:
url = self.base_url + "/embeddings"
with httpx.Client(timeout=60) as client:
response = client.post(
url,
json={"model": self.model_name, "input": texts},
headers={"Authorization": f"Bearer {self.api_key}"},
)
return [item["embedding"] for item in response.json()["data"]]
if __name__ == "__main__":
answer = asyncio.run(OpenaiAPI.open_api_chat_without_thinking(
query="你好",
json_output=True,
system_prompt="你是一个助手,请用中文回答问题。请直接给出最终答案,不要包含任何推理步骤、思考过程、解释性文字或前缀。",
enable_thinking=True
))
print(answer)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1023 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -968,7 +968,7 @@ FAULT_DIAGNOSIS_PROMPTS = {
" - 示例:\"生成报告\"\"写报告\"\"导出报告\"\"下载报告\"\"保存报告\"\n" " - 示例:\"生成报告\"\"写报告\"\"导出报告\"\"下载报告\"\"保存报告\"\n"
"\n" "\n"
"2. modify_info: 用户需要修改或补充之前提供的故障信息(舷号、设备名称、故障现象等)\n" "2. modify_info: 用户需要修改或补充之前提供的故障信息(舷号、设备名称、故障现象等)\n"
" - 示例:\"其实是...\"\"不对,应该是...\"\"我补充一下...\"\"修改设备名称是...\"\"更正一下,故障是...\"\"我需要补充...\"\n" " - 示例:\"其实是...\"\"不对,应该是...\"\"我补充一下...\"\"修改设备名称是...\"\"更正一下,故障是...\"\n"
"\n" "\n"
"3. has_error: 用户指出之前生成的维修方案有错误、需要修改或调整方案内容\n" "3. has_error: 用户指出之前生成的维修方案有错误、需要修改或调整方案内容\n"
" - 示例:\"方案有误\"\"这个步骤不对\"\"需要修改方案\"\"这个方案有问题\"\n" " - 示例:\"方案有误\"\"这个步骤不对\"\"需要修改方案\"\"这个方案有问题\"\n"

124
test_ship_mapping.py Normal file
View File

@ -0,0 +1,124 @@
"""
智能舷号映射 + RAG检索 完整测试脚本
直接调用 search_with_ship_number_strategy展示真实检索结果
"""
import asyncio
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from utils.ship_number_search import search_with_ship_number_strategy, smart_ship_number_mapping
async def interactive_test():
"""交互式完整测试"""
print("=" * 60)
print("智能舷号映射 + RAG检索 完整测试")
print("=" * 60)
print("输入 'exit''quit' 退出")
print("输入 'help' 查看说明")
print("=" * 60)
while True:
print("\n" + "-" * 60)
ship_input = input("请输入舷号/型号/舰名: ").strip()
if ship_input.lower() in ['exit', 'quit', 'q']:
print("退出测试")
break
if ship_input.lower() == 'help':
print("\n使用说明:")
print(" 先输入舷号/型号/舰名(如 163、南昌舰、055驱逐舰")
print(" 再输入查询内容(如 主机排烟温度高)")
print(" 系统会执行完整的智能映射 + RAG三步检索")
continue
if not ship_input:
print("请输入有效内容")
continue
query_input = input("请输入查询内容: ").strip()
if not query_input:
query_input = "主机排烟温度高的维修方案"
print(f"使用默认查询: {query_input}")
search_type = input("搜索类型 (fault/operate回车默认fault): ").strip()
if search_type not in ["fault", "operate"]:
search_type = "fault"
print("\n" + "=" * 60)
print("第一步:智能映射")
print("=" * 60)
try:
matched_ship_number, matched_model, all_numbers, match_type = await smart_ship_number_mapping(ship_input)
print(f"输入: '{ship_input}'")
print(f"匹配类型: {match_type}")
print(f"匹配舷号: {matched_ship_number}")
print(f"匹配型号: {matched_model}")
print(f"该型号所有舷号: {all_numbers}")
if match_type == "none":
print("未匹配到,将使用原始输入进行检索")
except Exception as e:
print(f"映射失败: {str(e)}")
matched_ship_number = None
matched_model = None
all_numbers = None
match_type = "none"
print("\n" + "=" * 60)
print("第二步RAG检索三步策略")
print("=" * 60)
try:
rag_results, matched_kb_name, matched_kb_id = await search_with_ship_number_strategy(
base_query=query_input,
ship_number=ship_input,
top_k=5,
search_type=search_type
)
print(f"\n检索完成!")
print(f"知识库: {matched_kb_name or '未匹配'}")
print(f"知识库ID: {matched_kb_id or '未匹配'}")
print(f"结果数量: {len(rag_results)}")
if rag_results:
print(f"\n{'=' * 60}")
print("第三步:检索结果详情")
print("=" * 60)
for i, item in enumerate(rag_results, 1):
if isinstance(item, dict):
text = item.get("text", "")
kb_name = item.get("kb_name", "")
score = item.get("score", "")
source = item.get("source", "")
print(f"\n--- 结果 {i} ---")
if kb_name:
print(f"知识库: {kb_name}")
if score:
print(f"相似度: {score}")
if source:
print(f"来源: {source}")
if text:
preview = text[:300] + "..." if len(text) > 300 else text
print(f"内容预览:\n{preview}")
elif isinstance(item, str):
preview = item[:300] + "..." if len(item) > 300 else item
print(f"\n--- 结果 {i} ---")
print(f"内容预览:\n{preview}")
else:
print("\n未检索到任何结果")
except Exception as e:
print(f"\n检索失败: {str(e)}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(interactive_test())

View File

@ -9,21 +9,21 @@ from config import POSTGRES_CONNECTION_STRING
ROUTE_FLAG_DISPLAY_NAMES = { ROUTE_FLAG_DISPLAY_NAMES = {
"故障排查与修理": "故障排查与修理", "问题排查": "问题排查",
"操作使用": "操作使用", "操作使用": "操作使用",
"舰船百科": "舰船百科" "百科问答": "百科问答"
} }
ROUTE_FLAG_ICONS = { ROUTE_FLAG_ICONS = {
"故障排查与修理": "detection", "问题排查": "detection",
"操作使用": "repair", "操作使用": "repair",
"舰船百科": "doc" "百科问答": "doc"
} }
ROUTE_FLAG_COLORS = { ROUTE_FLAG_COLORS = {
"故障排查与修理": "#FFB800", "问题排查": "#FFB800",
"操作使用": "#00C2FF", "操作使用": "#00C2FF",
"舰船百科": "#9153FF" "百科问答": "#9153FF"
} }
@ -131,7 +131,7 @@ async def get_agent_usage_statistics() -> Dict[str, Any]:
print("[agent_usage_statistics] psycopg_pool 未安装,无法查询") print("[agent_usage_statistics] psycopg_pool 未安装,无法查询")
return {"success": False, "data": None} return {"success": False, "data": None}
required_routes = ["操作使用", "故障排查与修理", "舰船百科"] required_routes = ["操作使用", "问题排查", "百科问答"]
now = datetime.now() now = datetime.now()
last_6_months = [] last_6_months = []

222
tools/config.py Normal file
View File

@ -0,0 +1,222 @@
"""
项目配置文件
.env文件读取配置
"""
import os
from dotenv import load_dotenv
from contextvars import ContextVar
from typing import Optional, Dict, Any
# 加载.env文件
load_dotenv()
# ==================== 大模型配置 ====================
LLM_CONFIG = {
# "model": "Qwen3.5-35B-A3B",
"model": "Qwen3-32B",
# "model": "Qwen3-14B",
"base_url": "http://192.168.1.164:9800/v1",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"temperature": float("0.1"),
"max_tokens": int("4096"),
"timeout": int("30")
}
# ==================== 嵌入模型配置 ====================
EMBEDDING_CONFIG = {
"model": "bge-m3",
"base_url": "http://embedding:9700/v1/embeddings",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"base_url_v2" : "http://embedding:9700/v1",
}
# ==================== 线程安全的请求上下文配置 ====================
# 使用 contextvars 存储每个请求的用户配置,确保多用户并发时不会相互干扰
_request_user_config: ContextVar[Optional[Dict[str, str]]] = ContextVar('request_user_config', default=None)
# RAG配置的默认值
_RAG_CONFIG_DEFAULT = {
"search_endpoint": "http://htknow:8080/api/v1/knowledge/search/",
"x-user-id": "1",
"x-user-name": "testuser",
"x-role": "admin",
"kb-id": None,
"file-id": None
}
class DynamicRAGConfig(dict):
"""
动态RAG配置类支持线程安全的请求级配置
当访问用户相关配置时自动从当前请求上下文读取
其他配置项直接返回默认值
兼容字典的所有操作方式 get(), [], in
"""
def __getitem__(self, key: str):
# 如果是用户相关配置或知识库参数,从当前请求上下文读取
if key in ["x-user-id", "x-user-name", "x-role", "kb-id", "file-id"]:
request_config = _request_user_config.get()
if request_config and key in request_config:
return request_config[key]
# 如果没有设置,返回默认值
return _RAG_CONFIG_DEFAULT[key]
# 其他配置项直接返回默认值
return _RAG_CONFIG_DEFAULT.get(key)
def get(self, key: str, default=None):
"""支持 get() 方法"""
try:
return self[key]
except KeyError:
return default
def __contains__(self, key: str):
"""支持 in 操作符"""
return key in _RAG_CONFIG_DEFAULT
def keys(self):
"""返回所有键"""
return _RAG_CONFIG_DEFAULT.keys()
def values(self):
"""返回所有值(动态读取用户配置)"""
return [self[key] for key in _RAG_CONFIG_DEFAULT.keys()]
def items(self):
"""返回所有键值对(动态读取用户配置)"""
return [(key, self[key]) for key in _RAG_CONFIG_DEFAULT.keys()]
# ==================== RAG服务配置 ====================
# 使用动态配置类,支持多用户并发时自动读取各自的配置
RAG_CONFIG = DynamicRAGConfig()
def set_request_user_config(x_user_id: Optional[str] = None,
x_user_name: Optional[str] = None,
x_role: Optional[str] = None,
text_kb_id: Optional[str] = None,
text_file_id: Optional[str] = None):
"""
设置当前请求的用户配置和知识库参数线程安全
Args:
x_user_id: 用户ID
x_user_name: 用户名称
x_role: 角色
text_kb_id: 文本检索知识库ID
text_file_id: 文本检索文件ID
"""
config = {}
if x_user_id is not None:
config["x-user-id"] = x_user_id
if x_user_name is not None:
config["x-user-name"] = x_user_name
if x_role is not None:
config["x-role"] = x_role
# 仅当非空字符串时才设置,空字符串时不传入 RAG 接口
if text_kb_id is not None and str(text_kb_id).strip():
config["kb-id"] = str(text_kb_id).strip()
if text_file_id is not None and str(text_file_id).strip():
config["file-id"] = str(text_file_id).strip()
_request_user_config.set(config if config else None)
# ==================== 图谱检索服务配置 ====================
GRAPH_SEARCH_CONFIG = {
"search_endpoint": "http://kgrag:9085/search_graph",
"graph_timeout": float("30.0"),
"default_top_k": int("10"),
"operation_search_endpoint": "http://kgrag:9085/operation_graph_search"
}
# ==================== 设备到系统检索服务配置 ====================
DEVICE_SYSTEM_CONFIG = {
"endpoint": "http://kgrag:9085/device_to_system",
"timeout": float("30.0"),
"default_min_hops": int("2"),
"default_max_hops": int("8"),
"default_top_k": int("10")
}
# ==================== 图册检索服务配置 ====================
ATLAS_CONFIG = {
"endpoint": "http://kgrag:9085/atlas_retrieval",
"timeout": float("30.0"),
"default_top_k": int("10")
}
# ==================== 工作流配置 ====================
WORKFLOW_CONFIG = {
"max_retry_count": int("2"), # 最大重试次数
"image_clarity_threshold": float("0.7"), # 图片清晰度阈值
"default_timeout": int("30") # 默认超时时间(秒)
}
# ==================== 服务器配置 ====================
SERVER_CONFIG = {
"host": "agent",
"port": "9088",
"base_url": "http://agent:9088"
}
# ==================== ASR服务配置 ====================
ASR_CONFIG = {
"api_base": "http://asr:9900/v1",
"model": "base",
"language": "zh",
"timeout": int("30")
}
# ==================== VLM服务配置 ====================
VLM_CONFIG = {
"api_base" : "http://kgrag:9085/v1",
"model" : "Qwen3-VL-8B"
}
# ==================== Neo4J配置 ====================
NEO4J_CONFIG = {
"uri": "neo4j://neo4j:7687",
"user": "neo4j",
"password": "zdht123"
}
# ==================== 知识库匹配配置 ====================
KB_TREE_CONFIG = {
"url": "http://htknow:8080/api/v1/knowledge/knowledge_base/tree"
}
# ==================== 维修反馈统计配置 ====================
REPAIR_FEEDBACK_CONFIG = {
"url": "http://webui:22000",
"email": "15088888888@163.com",
"password": "11223344"
}
# ==================== 数据库配置 ====================
POSTGRES_CONNECTION_STRING = "postgresql://postgres:zdht123@postgres:5432/hj_webui"
#POSTGRES_CONNECTION_STRING = "postgresql+psycopg://postgres:zdht123@postgres:5432/hj_webui"
POSTGRES_SQLALCHEMY_URL = "postgresql+psycopg://postgres:zdht123@postgres:5432/hj_webui"
# ==================== 索引配置 ====================
INDEX_CONFIG = {
"NAME_PROPERTY" : "名称",
"FULLTEXT_PROPERTY" :"fulltext",
"EMBEDDING_PROPERTY" : "embedding",
"SEARCH_LABEL" : "Searchable",
"VECTOR_INDEX_NAME" : "global_searchable_embedding",
"FULLTEXT_INDEX_NAME" : "global_searchable_content_search"
}
# ==================== sql表格字段与neo4j节点映射 ====================
MAP_INS={
"故障模式" : "fault",
"系统" : "system_name",
"设备" : "device_name",
"舷号" : "ship_number",
}
# ==================== neo4j配置 ====================
NEO4J_CONFIG = {
"uri": "neo4j://neo4j:7687",
"user": "neo4j",
"password": "zdht123@"
}

View File

@ -49,39 +49,64 @@ async def get_device_system_from_neo4j(device_name: str) -> str:
if device_name_clean in neo4j_cache: if device_name_clean in neo4j_cache:
return neo4j_cache[device_name_clean] return neo4j_cache[device_name_clean]
try: from config import DEVICE_SYSTEM_CONFIG
from tools.graph_tools import find_system_by_device
result_json = await find_system_by_device( endpoint = DEVICE_SYSTEM_CONFIG.get("endpoint")
device_name=device_name.strip(), timeout = DEVICE_SYSTEM_CONFIG.get("timeout", 30.0)
min_hops=2, min_hops = DEVICE_SYSTEM_CONFIG.get("default_min_hops", 2)
max_hops=8, max_hops = DEVICE_SYSTEM_CONFIG.get("default_max_hops", 8)
top_k=10, top_k = DEVICE_SYSTEM_CONFIG.get("default_top_k", 10)
)
if not result_json.get("success", False):
result = "其他"
neo4j_cache[device_name_clean] = result
return result
systems = result_json.get("systems", [])
if not systems or not isinstance(systems, list):
result = "其他"
neo4j_cache[device_name_clean] = result
return result
first_system = systems[0]
if isinstance(first_system, dict):
system_name = first_system.get("name") or first_system.get("系统名称") or first_system.get("名称")
if system_name:
result = system_name.strip()
neo4j_cache[device_name_clean] = result
return result
if not endpoint:
result = "其他" result = "其他"
neo4j_cache[device_name_clean] = result neo4j_cache[device_name_clean] = result
return result return result
try:
payload = {
"device_name": device_name.strip(),
"min_hops": min_hops,
"max_hops": max_hops,
"top_k": top_k
}
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
endpoint,
json=payload,
headers={"Content-Type": "application/json"}
)
if response.status_code != 200:
result = "其他"
neo4j_cache[device_name_clean] = result
return result
result_json = response.json()
if not result_json.get("success", False):
result = "其他"
neo4j_cache[device_name_clean] = result
return result
systems = result_json.get("systems", [])
if not systems or not isinstance(systems, list):
result = "其他"
neo4j_cache[device_name_clean] = result
return result
first_system = systems[0]
if isinstance(first_system, dict):
system_name = first_system.get("name") or first_system.get("系统名称") or first_system.get("名称")
if system_name:
result = system_name.strip()
neo4j_cache[device_name_clean] = result
return result
result = "其他"
neo4j_cache[device_name_clean] = result
return result
except Exception as e: except Exception as e:
print(f"查询设备系统失败: {str(e)}") print(f"查询设备系统失败: {str(e)}")
result = "其他" result = "其他"
@ -941,3 +966,4 @@ async def repair_feedback_statistics_tool(
) )
return result return result

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1076
tools/graph_tools623.py Normal file

File diff suppressed because it is too large Load Diff

1093
tools/graph_tools624.py Normal file

File diff suppressed because it is too large Load Diff

208
tools/ship_model_db528.py Normal file
View File

@ -0,0 +1,208 @@
"""
舷号-型号映射数据库模块
用于管理舰船型号舷号别名舰名的映射关系
数据存储在 PostgreSQL ship_model_mapping 表中
"""
import json
from typing import Dict, List, Any, Optional
from config import POSTGRES_CONNECTION_STRING
async def init_ship_model_mapping_table():
"""
初始化 ship_model_mapping 表并插入初始数据
"""
try:
from psycopg_pool import AsyncConnectionPool
except ImportError:
print("[ship_model_db] psycopg_pool 未安装,无法初始化表")
return
async with AsyncConnectionPool(
POSTGRES_CONNECTION_STRING,
kwargs={"autocommit": True}
) as pool:
async with pool.connection() as conn:
async with conn.cursor() as cur:
await cur.execute("""
CREATE TABLE IF NOT EXISTS ship_model_mapping (
id SERIAL PRIMARY KEY,
model_name TEXT NOT NULL UNIQUE,
numbers JSONB NOT NULL DEFAULT '[]',
aliases JSONB NOT NULL DEFAULT '[]',
names JSONB NOT NULL DEFAULT '[]'
)
""")
await cur.execute("""
CREATE INDEX IF NOT EXISTS ship_model_mapping_model_name_idx ON ship_model_mapping(model_name)
""")
print("[ship_model_db] ship_model_mapping 表初始化完成")
# 检查是否已有数据,没有则插入初始数据
await cur.execute("SELECT COUNT(*) FROM ship_model_mapping")
count = (await cur.fetchone())[0]
if count == 0:
initial_data = [
("055型驱逐舰", ["102", "103", "104", "105", "106", "107", "108"],
["055", "055型", "055驱逐舰", "万吨大驱"],
["拉萨舰", "鞍山舰", "无锡舰", "大连舰", "延安舰", "遵义舰", "咸阳舰"]),
("052D型驱逐舰", ["172", "173", "174", "175", "163"],
["052D", "052D型", "052D驱逐舰"],
["昆明舰", "长沙舰", "合肥舰", "银川舰", "南昌舰"]),
("054A型护卫舰", ["529", "530", "547", "568", "570"],
["054A", "054A型", "054A护卫舰"],
["舟山舰", "徐州舰", "临沂舰", "衡阳舰", "黄山舰"]),
]
for model_name, numbers, aliases, names in initial_data:
await cur.execute(
"""
INSERT INTO ship_model_mapping (model_name, numbers, aliases, names)
VALUES (%s, %s, %s, %s)
""",
(model_name,
json.dumps(numbers, ensure_ascii=False),
json.dumps(aliases, ensure_ascii=False),
json.dumps(names, ensure_ascii=False))
)
print(f"[ship_model_db] 已插入 {len(initial_data)} 条初始数据")
async def load_ship_model_mapping() -> Dict[str, Dict[str, List[str]]]:
"""
从数据库加载全部舷号-型号映射数据
Returns:
与原 config.SHIP_MODEL_MAPPING 格式一致的字典
{
"055型驱逐舰": {
"numbers": ["102", ...],
"aliases": ["055", ...],
"names": ["拉萨舰", ...]
},
...
}
"""
try:
from psycopg_pool import AsyncConnectionPool
except ImportError:
print("[ship_model_db] psycopg_pool 未安装,返回空映射")
return {}
try:
async with AsyncConnectionPool(
POSTGRES_CONNECTION_STRING,
kwargs={"autocommit": True}
) as pool:
async with pool.connection() as conn:
async with conn.cursor() as cur:
await cur.execute(
"SELECT model_name, numbers, aliases, names FROM ship_model_mapping"
)
rows = await cur.fetchall()
result = {}
for row in rows:
model_name, numbers, aliases, names = row
# JSONB 返回的可能是 list 或 str统一处理
if isinstance(numbers, str):
numbers = json.loads(numbers)
if isinstance(aliases, str):
aliases = json.loads(aliases)
if isinstance(names, str):
names = json.loads(names)
result[model_name] = {
"numbers": numbers,
"aliases": aliases,
"names": names,
}
return result
except Exception as e:
print(f"[ship_model_db] 加载映射数据失败: {str(e)}")
return {}
async def add_ship_model(
model_name: str,
numbers: List[str],
aliases: List[str],
names: List[str]
) -> bool:
"""
添加舰船型号映射
Args:
model_name: 型号名称
numbers: 舷号列表
aliases: 别名列表
names: 舰名列表
Returns:
是否添加成功
"""
try:
from psycopg_pool import AsyncConnectionPool
except ImportError:
print("[ship_model_db] psycopg_pool 未安装,无法添加")
return False
try:
async with AsyncConnectionPool(
POSTGRES_CONNECTION_STRING,
kwargs={"autocommit": True}
) as pool:
async with pool.connection() as conn:
async with conn.cursor() as cur:
await cur.execute(
"""
INSERT INTO ship_model_mapping (model_name, numbers, aliases, names)
VALUES (%s, %s, %s, %s)
ON CONFLICT (model_name) DO UPDATE SET
numbers = EXCLUDED.numbers,
aliases = EXCLUDED.aliases,
names = EXCLUDED.names
""",
(model_name,
json.dumps(numbers, ensure_ascii=False),
json.dumps(aliases, ensure_ascii=False),
json.dumps(names, ensure_ascii=False))
)
print(f"[ship_model_db] 舰船型号 '{model_name}' 已保存")
return True
except Exception as e:
print(f"[ship_model_db] 添加舰船型号失败: {str(e)}")
return False
async def delete_ship_model(model_name: str) -> bool:
"""
删除舰船型号映射
Args:
model_name: 型号名称
Returns:
是否删除成功
"""
try:
from psycopg_pool import AsyncConnectionPool
except ImportError:
print("[ship_model_db] psycopg_pool 未安装,无法删除")
return False
try:
async with AsyncConnectionPool(
POSTGRES_CONNECTION_STRING,
kwargs={"autocommit": True}
) as pool:
async with pool.connection() as conn:
async with conn.cursor() as cur:
await cur.execute(
"DELETE FROM ship_model_mapping WHERE model_name = %s",
(model_name,)
)
print(f"[ship_model_db] 舰船型号 '{model_name}' 已删除")
return True
except Exception as e:
print(f"[ship_model_db] 删除舰船型号失败: {str(e)}")
return False

View File

@ -0,0 +1,467 @@
"""
舷号搜索策略模块
提供三级搜索策略
1. 指定舷号搜索
2. 同型号所有舷号搜索
3. 全局搜索
新增功能
- 智能舷号映射支持舷号型号舰名的智能识别和映射
"""
from typing import List, Dict, Any, Optional, Tuple
import random
import re
import httpx
from utils.function_tracker import get_all_callbacks
from tools.ship_model_db import load_ship_model_mapping
from tools.rag_tools import rag_search
from modelsAPI.model_api import OpenaiAPI
from config import KB_TREE_CONFIG
# ==================== 舷号 -> kb_id 映射 ====================
_kb_name_to_id_cache: Optional[Dict[str, str]] = None
async def _load_kb_tree() -> Dict[str, str]:
"""
从知识库树 API 加载所有知识库返回 {名称: kb_id} 的映射
递归遍历树结构带缓存只请求一次
"""
global _kb_name_to_id_cache
if _kb_name_to_id_cache is not None:
return _kb_name_to_id_cache
url = KB_TREE_CONFIG.get("url", "")
if not url:
print("[ship_number_search] KB_TREE_CONFIG.url 未配置")
return {}
headers = {
"accept": "application/json",
"x-user-id": "1",
"x-user-name": "testuser",
"x-role": "admin",
}
try:
async with httpx.AsyncClient(timeout=30.0, verify=False) as client:
response = await client.get(url, headers=headers)
if response.status_code != 200:
print(f"[ship_number_search] 知识库树 API 请求失败: HTTP {response.status_code}")
return {}
result = response.json()
# API 返回的是列表,每个元素可能有 children
if isinstance(result, list):
top_nodes = result
elif isinstance(result, dict):
kb_data = result.get("data") or result.get("list") or {}
top_nodes = kb_data.get("children", []) if isinstance(kb_data, dict) else []
else:
top_nodes = []
# 递归收集所有知识库
name_to_id = {}
def _collect(nodes):
for node in nodes:
kb_name = node.get("name") or ""
kb_id = str(node.get("id", "")) if node.get("id") is not None else ""
if kb_name and kb_id:
name_to_id[kb_name] = kb_id
children = node.get("children", [])
if children:
_collect(children)
_collect(top_nodes)
_kb_name_to_id_cache = name_to_id
return name_to_id
except Exception as e:
print(f"[ship_number_search] 加载知识库树失败: {str(e)}")
return {}
async def resolve_kb_id(ship_number: str) -> Optional[str]:
"""
根据舷号从知识库树中查找对应的真实 kb_id
匹配策略在知识库名称中查找包含该舷号的条目
例如 ship_number="163"知识库名称为 "163舰资料"则匹配成功
Args:
ship_number: 舷号 "163"
Returns:
匹配到的 kb_id未找到返回 None
"""
if not ship_number:
return None
name_to_id = await _load_kb_tree()
# 优先精确匹配:名称就是舷号本身
if ship_number in name_to_id:
return name_to_id[ship_number]
# 其次:名称包含该舷号(确保是完整数字,避免 "16" 匹配到 "163"
for kb_name, kb_id in name_to_id.items():
# 用正则匹配完整数字,确保舷号作为独立数字出现在名称中
if re.search(rf'(?<!\d){re.escape(ship_number)}(?!\d)', kb_name):
return kb_id
return None
async def get_ship_numbers_by_model(ship_number: str) -> list:
"""
根据舷号获取同型号的所有舷号
Args:
ship_number: 舷号
Returns:
list: 同型号的所有舷号列表如果未找到则返回只包含输入舷号的列表
"""
ship_number = str(ship_number).strip()
ship_model_mapping = await load_ship_model_mapping()
for model, info in ship_model_mapping.items():
numbers = info.get("numbers", [])
if ship_number in numbers:
return numbers.copy()
return [ship_number]
async def _build_ship_mapping_candidates() -> Dict[str, Dict[str, Any]]:
"""
构建舷号映射候选词库
舰名与舷号按位置一一对应 names[0] 对应 numbers[0]
"""
candidates = {}
ship_model_mapping = await load_ship_model_mapping()
for model_name, info in ship_model_mapping.items():
numbers = info.get("numbers", [])
aliases = info.get("aliases", [])
names = info.get("names", [])
mapping_info = {
"model": model_name,
"numbers": numbers.copy()
}
for num in numbers:
candidates[str(num)] = {
"type": "number",
"ship_number": str(num),
**mapping_info
}
for alias in aliases:
if alias:
candidates[alias] = {
"type": "alias",
"ship_number": None,
**mapping_info
}
for idx, name in enumerate(names):
if name:
ship_num = str(numbers[idx]) if idx < len(numbers) else None
candidates[name] = {
"type": "name",
"ship_number": ship_num,
**mapping_info
}
return candidates
_SHIP_MAPPING_CANDIDATES = None
async def get_ship_mapping_candidates() -> Dict[str, Dict[str, Any]]:
"""
获取舷号映射候选词库带缓存
"""
global _SHIP_MAPPING_CANDIDATES
if _SHIP_MAPPING_CANDIDATES is None:
_SHIP_MAPPING_CANDIDATES = await _build_ship_mapping_candidates()
return _SHIP_MAPPING_CANDIDATES
async def smart_ship_number_mapping(
user_input: str,
similarity_threshold: float = 0.8
) -> Tuple[Optional[str], Optional[str], Optional[List[str]], str]:
"""
智能舷号映射函数
将用户输入的舷号/型号/舰名映射到准确的舷号或型号
Returns:
Tuple: (matched_ship_number, matched_model, all_numbers, match_type)
- matched_ship_number: 匹配到的具体舷号舰名也会映射到对应舷号
- matched_model: 匹配到的型号名称
- all_numbers: 该型号下的所有舷号列表
- match_type: 匹配类型 ("exact_number", "exact_alias", "exact_name", "embedding", "none")
"""
if not user_input or not user_input.strip():
return None, None, None, "none"
user_input = user_input.strip()
candidates = await get_ship_mapping_candidates()
if user_input in candidates:
info = candidates[user_input]
match_type = f"exact_{info['type']}"
model = info["model"]
numbers = info["numbers"]
ship_number = info.get("ship_number")
return ship_number, model, numbers, match_type
try:
candidate_texts = list(candidates.keys())
query_embedding = await OpenaiAPI.get_embeddings_async(user_input)
candidate_embeddings = await OpenaiAPI.get_embeddings_batch_async(candidate_texts)
best_score = -1
best_idx = -1
for idx, emb in enumerate(candidate_embeddings):
score = OpenaiAPI.cosine_similarity(query_embedding, emb)
if score > best_score:
best_score = score
best_idx = idx
if best_score >= similarity_threshold and best_idx >= 0:
matched_text = candidate_texts[best_idx]
info = candidates[matched_text]
model = info["model"]
numbers = info["numbers"]
ship_number = info.get("ship_number")
print(f"[智能映射] 用户输入 '{user_input}' -> 匹配到 '{matched_text}' (相似度: {best_score:.3f}, 类型: {info['type']})")
return ship_number, model, numbers, "embedding"
except Exception as e:
print(f"[智能映射] Embedding 匹配失败: {str(e)}")
return None, None, None, "none"
def send_search_status(title: str, details: str):
"""发送搜索状态事件"""
title_options = [f"🔍 {title}", f"🔎 {title}", f"📚 {title}"]
start_event = {
"type": "function_execution",
"title": random.choice(title_options),
"details": details
}
for callback in get_all_callbacks():
try:
callback(start_event)
except Exception:
pass
def convert_rag_result(rag_result):
"""将 rag_search 的返回结果转换为原来的格式"""
if not rag_result or not rag_result.get("success", False):
return []
source_citation = rag_result.get("sourceCitation", {})
flat_results = []
for resource, items in source_citation.items():
for item in items:
flat_results.append(item)
return flat_results
async def search_with_ship_number_strategy(
base_query: str,
ship_number: str,
top_k: int = 5,
search_type: str = "fault"
) -> Tuple[List[Dict[str, Any]], str, str, str]:
"""
使用舷号三级搜索策略进行RAG检索
返回: (rag_results, matched_kb_name, matched_kb_id, matched_ship_number)
matched_ship_number: 映射后的舷号未映射到具体舷号时为空字符串
"""
rag_results = []
matched_kb_name = ""
matched_kb_id = ""
matched_ship_number = None
matched_model = None
model_all_numbers = None
match_type = "none"
if ship_number and ship_number.strip():
send_search_status(
"正在智能识别",
f"识别 '{ship_number}' 的含义(舷号/型号/舰名)..."
)
matched_ship_number, matched_model, model_all_numbers, match_type = await smart_ship_number_mapping(ship_number)
if match_type != "none":
print(f"[智能映射结果] 输入='{ship_number}' -> 舷号={matched_ship_number}, 型号={matched_model}, 所有舷号={model_all_numbers}, 匹配类型={match_type}")
else:
print(f"[智能映射结果] 输入='{ship_number}' -> 未找到匹配,将使用原始值进行检索")
if match_type in ["exact_number", "exact_name", "embedding"] and matched_ship_number:
real_kb_id = await resolve_kb_id(matched_ship_number)
if real_kb_id:
send_search_status(
"正在指定舷号检索",
f"使用舷号 {matched_ship_number} 进行检索..."
)
rag_query = f"{base_query}"
try:
rag_result_raw = await rag_search(rag_query, top_k=top_k, kb_id=real_kb_id)
rag_results = convert_rag_result(rag_result_raw)
if rag_results:
print(f"步骤1 - 舷号 {matched_ship_number} 检索完成,返回 {len(rag_results)} 条结果")
for item in rag_results:
if isinstance(item, dict):
found_kb_name = item.get("kb_name", "")
found_kb_id = item.get("kb_id", "")
if found_kb_name and not matched_kb_name:
matched_kb_name = str(found_kb_name).strip()
if found_kb_id and not matched_kb_id:
matched_kb_id = str(found_kb_id).strip()
return rag_results, matched_kb_name, matched_kb_id, matched_ship_number or ""
except Exception as e:
print(f"步骤1 - 舷号 {matched_ship_number} 检索失败: {str(e)}")
else:
print(f"步骤1 - 舷号 {matched_ship_number} 无对应知识库,跳过指定舷号检索")
if match_type in ["exact_number", "exact_alias", "exact_name", "embedding"] and model_all_numbers:
send_search_status(
"正在型号检索",
f"识别为型号 '{matched_model}',使用该型号所有舷号 {', '.join(model_all_numbers)} 进行检索..."
)
for num in model_all_numbers:
rag_query = f"{base_query}"
try:
real_kb_id = await resolve_kb_id(num)
if not real_kb_id:
continue
rag_result_raw = await rag_search(rag_query, top_k=top_k, kb_id=real_kb_id)
current_results = convert_rag_result(rag_result_raw)
if current_results:
rag_results.extend(current_results)
print(f"步骤2 - 舷号 {num} 检索完成,返回 {len(current_results)} 条结果")
if not matched_kb_name:
for item in current_results:
if isinstance(item, dict):
found_kb_name = item.get("kb_name", "")
found_kb_id = item.get("kb_id", "")
if found_kb_name and not matched_kb_name:
matched_kb_name = str(found_kb_name).strip()
if found_kb_id and not matched_kb_id:
matched_kb_id = str(found_kb_id).strip()
except Exception as e:
print(f"步骤2 - 舷号 {num} 检索失败: {str(e)}")
if rag_results:
return rag_results, matched_kb_name, matched_kb_id, matched_ship_number or ""
if ship_number and ship_number.strip() and match_type == "none":
real_kb_id = await resolve_kb_id(ship_number)
if real_kb_id:
send_search_status(
"正在指定舰名检索",
f"使用舰名 {ship_number} 进行检索..."
)
rag_query = f"{base_query}"
try:
rag_result_raw = await rag_search(rag_query, top_k=top_k, kb_id=real_kb_id)
rag_results = convert_rag_result(rag_result_raw)
if rag_results:
print(f"步骤1 - 舷号 {ship_number} 检索完成,返回 {len(rag_results)} 条结果")
for item in rag_results:
if isinstance(item, dict):
found_kb_name = item.get("kb_name", "")
found_kb_id = item.get("kb_id", "")
if found_kb_name and not matched_kb_name:
matched_kb_name = str(found_kb_name).strip()
if found_kb_id and not matched_kb_id:
matched_kb_id = str(found_kb_id).strip()
return rag_results, matched_kb_name, matched_kb_id, matched_ship_number or ""
except Exception as e:
print(f"步骤1 - 舷号 {ship_number} 检索失败: {str(e)}")
else:
print(f"步骤1 - 舷号 {ship_number} 无对应知识库,跳过指定舰名检索")
model_ship_numbers = await get_ship_numbers_by_model(ship_number)
if len(model_ship_numbers) > 1 or (len(model_ship_numbers) == 1 and ship_number not in model_ship_numbers):
send_search_status(
"正在同型号检索",
f"使用同型号舷号 {', '.join(model_ship_numbers)} 进行检索..."
)
for num in model_ship_numbers:
if num == ship_number:
continue
rag_query = f"{base_query}"
try:
real_kb_id = await resolve_kb_id(num)
if not real_kb_id:
continue
rag_result_raw = await rag_search(rag_query, top_k=top_k, kb_id=real_kb_id)
current_results = convert_rag_result(rag_result_raw)
if current_results:
rag_results.extend(current_results)
print(f"步骤2 - 舷号 {num} 检索完成,返回 {len(current_results)} 条结果")
if not matched_kb_name:
for item in current_results:
if isinstance(item, dict):
found_kb_name = item.get("kb_name", "")
found_kb_id = item.get("kb_id", "")
if found_kb_name and not matched_kb_name:
matched_kb_name = str(found_kb_name).strip()
if found_kb_id and not matched_kb_id:
matched_kb_id = str(found_kb_id).strip()
except Exception as e:
print(f"步骤2 - 舷号 {num} 检索失败: {str(e)}")
if rag_results:
return rag_results, matched_kb_name, matched_kb_id, matched_ship_number or ""
send_search_status(
"正在全局检索",
"未找到舷号相关资料,进行全局检索..."
)
try:
rag_result_raw = await rag_search(base_query, top_k=top_k)
rag_results = convert_rag_result(rag_result_raw)
if rag_results:
print(f"步骤3 - 全局检索完成,返回 {len(rag_results)} 条结果")
for item in rag_results:
if isinstance(item, dict):
found_kb_name = item.get("kb_name", "")
found_kb_id = item.get("kb_id", "")
if found_kb_name and not matched_kb_name:
matched_kb_name = str(found_kb_name).strip()
if found_kb_id and not matched_kb_id:
matched_kb_id = str(found_kb_id).strip()
except Exception as e:
print(f"步骤3 - 全局检索失败: {str(e)}")
rag_results = []
return rag_results, matched_kb_name, matched_kb_id, matched_ship_number or ""

716
utils/word_generator518.py Normal file
View File

@ -0,0 +1,716 @@
import os
import re
import uuid
import logging
import tempfile
from datetime import datetime
from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from lxml import etree
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
_MML2OMML_XSLT = None
def _load_mml2omml_xslt():
global _MML2OMML_XSLT
if _MML2OMML_XSLT is not None:
return _MML2OMML_XSLT
search_paths = [
r"C:\Program Files\Microsoft Office\root\Office16\MML2OMML.XSL",
r"C:\Program Files (x86)\Microsoft Office\root\Office16\MML2OMML.XSL",
r"C:\Program Files\Microsoft Office\Office16\MML2OMML.XSL",
r"C:\Program Files (x86)\Microsoft Office\Office16\MML2OMML.XSL",
"/usr/share/mml2omml/MML2OMML.XSL",
]
for path in search_paths:
if os.path.isfile(path):
_MML2OMML_XSLT = etree.parse(path)
logger.info(f"加载 MML2OMML.XSL 成功: {path}")
return _MML2OMML_XSLT
logger.warning("未找到 MML2OMML.XSL公式将使用图片回退方式渲染")
return None
class MarkdownWordGenerator:
def __init__(self):
try:
self.doc = Document()
self.cache_dir = os.path.join(tempfile.gettempdir(), "latex_formula_cache")
os.makedirs(self.cache_dir, exist_ok=True)
self.formula_cache = {}
self._omml_available = _load_mml2omml_xslt() is not None
logger.info("MarkdownWordGenerator 初始化成功")
except Exception as e:
logger.error(f"初始化 MarkdownWordGenerator 失败: {e}")
raise
def set_font(self, run, size=11, bold=False):
try:
run.font.name = "微软雅黑"
rPr = run._element.get_or_add_rPr()
rFonts = rPr.find(qn('w:rFonts'))
if rFonts is None:
rFonts = run._element.makeelement(qn('w:rFonts'), {})
rPr.insert(0, rFonts)
rFonts.set(qn('w:eastAsia'), "微软雅黑")
run.font.size = Pt(size)
run.font.bold = True if bold else False
except Exception as e:
logger.warning(f"设置字体时出错: {e}")
def add_heading(self, text, level):
try:
p = self.doc.add_heading(level=level)
size_map = {1: 18, 2: 16, 3: 14}
font_size = size_map.get(level, 12)
self._add_rich_text(p, text, default_size=font_size, default_bold=True)
except Exception as e:
logger.error(f"添加标题失败 '{text}': {e}")
def add_paragraph(self, text):
try:
if not text.strip():
return
p = self.doc.add_paragraph()
indent_chars = 0
temp_text = text
while temp_text.startswith('\u3000'):
indent_chars += 1
temp_text = temp_text[1:]
if indent_chars >= 2:
p.paragraph_format.first_line_indent = Inches(0.5)
text = temp_text
elif indent_chars == 1:
p.paragraph_format.first_line_indent = Inches(0.25)
text = temp_text
self._add_rich_text(p, text)
except Exception as e:
logger.error(f"添加段落失败: {e}")
def _add_rich_text(self, paragraph, text, default_size=11, default_bold=False):
if not text:
return
pattern = r'\*\*(.+?)\*\*'
last_end = 0
for match in re.finditer(pattern, text):
start, end = match.span()
if start > last_end:
plain = text[last_end:start]
if plain:
run = paragraph.add_run(plain)
self.set_font(run, size=default_size, bold=default_bold)
bold_content = match.group(1)
if bold_content:
run = paragraph.add_run(bold_content)
self.set_font(run, size=default_size, bold=True)
last_end = end
if last_end < len(text):
remaining = text[last_end:]
if remaining:
run = paragraph.add_run(remaining)
self.set_font(run, size=default_size, bold=default_bold)
@staticmethod
def _preprocess_latex_for_omml(latex: str) -> str:
replacements = {
r'\leqslant': r'\leq',
r'\geqslant': r'\geq',
r'\nleqslant': r'\nleq',
r'\ngeqslant': r'\ngeq',
r'\eqslantless': r'\leq',
r'\eqslantgtr': r'\geq',
r'\varnothing': r'\emptyset',
r'\dfrac': r'\frac',
r'\tfrac': r'\frac',
r'\boldsymbol': r'\mathbf',
r'\bm': r'\mathbf',
}
for old, new in replacements.items():
latex = latex.replace(old, new)
latex = re.sub(r'\\mathrm\s*\{([^}]*)\}', lambda m: r'\mathrm{' + m.group(1) + '}', latex)
latex = re.sub(r'\\text\s*\{([^}]*)\}', lambda m: r'\mathrm{' + m.group(1) + '}', latex)
latex = re.sub(r'\\textbf\s*\{([^}]*)\}', lambda m: r'\mathbf{' + m.group(1) + '}', latex)
latex = re.sub(r'\\textit\s*\{([^}]*)\}', lambda m: r'\mathit{' + m.group(1) + '}', latex)
return latex
def _latex_to_omml(self, latex_str):
try:
import latex2mathml.converter
latex_str = self._preprocess_latex_for_omml(latex_str)
mathml = latex2mathml.converter.convert(latex_str)
mathml_tree = etree.fromstring(mathml.encode('utf-8'))
xslt = _load_mml2omml_xslt()
if xslt is None:
return None
transform = etree.XSLT(xslt)
omml_tree = transform(mathml_tree)
return omml_tree.getroot()
except Exception as e:
logger.error(f"LaTeX->OMML 转换失败 '{latex_str}': {e}")
return None
def _add_omml_to_paragraph(self, paragraph, latex_str):
omml = self._latex_to_omml(latex_str)
if omml is not None:
paragraph._element.append(omml)
return True
return False
def _add_rich_text_with_formulas(self, paragraph, text, default_size=11, default_bold=False):
if not text:
return
has_formula = bool(re.search(r'\$[^$]+?\$', text))
has_image = bool(self._extract_image_urls(text))
if not has_formula and not has_image:
self._add_rich_text(paragraph, text, default_size=default_size, default_bold=default_bold)
return
if has_image:
img_urls = self._extract_image_urls(text)
last_end = 0
for start, end, url in img_urls:
before_text = text[last_end:start]
if before_text and before_text.strip():
self._add_rich_text_with_formulas(paragraph, before_text, default_size=default_size, default_bold=default_bold)
self.add_image(url, width_inches=4.5)
last_end = end
after_text = text[last_end:]
if after_text and after_text.strip():
p = self.doc.add_paragraph()
self._add_rich_text_with_formulas(p, after_text, default_size=default_size, default_bold=default_bold)
return
segments = re.split(r'(\$[^$]+?\$)', text)
for segment in segments:
if not segment:
continue
if segment.startswith('$') and segment.endswith('$') and len(segment) > 2:
latex_content = segment[1:-1].strip()
if self._omml_available and self._add_omml_to_paragraph(paragraph, latex_content):
pass
else:
run = paragraph.add_run(segment)
self.set_font(run, size=default_size, bold=default_bold)
else:
self._add_rich_text(paragraph, segment, default_size=default_size, default_bold=default_bold)
def add_list(self, text, numbered=False, number=None):
try:
text = text.strip()
if not text:
return
if re.match(r'^[-_*]{2,}$', text):
return
if numbered and number is not None:
p = self.doc.add_paragraph()
p.paragraph_format.left_indent = Pt(18)
p.paragraph_format.first_line_indent = Pt(-18)
num_run = p.add_run(f"{number}. ")
self.set_font(num_run, bold=False)
self._add_rich_text_with_formulas(p, text)
else:
p = self.doc.add_paragraph(style="List Bullet")
self._add_rich_text_with_formulas(p, text)
except Exception as e:
logger.error(f"添加列表失败: {e}")
def add_sub_list(self, text):
try:
text = text.strip()
if not text:
return
if re.match(r'^[-_*]{2,}$', text):
return
try:
p = self.doc.add_paragraph(style="List Bullet 2")
except KeyError:
p = self.doc.add_paragraph(style="List Bullet")
p.paragraph_format.left_indent = Pt(36)
self._add_rich_text_with_formulas(p, text)
except Exception as e:
logger.error(f"添加子列表失败: {e}")
def _convert_url_to_local_path(self, url: str) -> str:
if not url:
return None
from urllib.parse import urlparse
path = urlparse(url).path
if path.startswith('/picture/'):
docker_path = f"/app/picture/{path[len('/picture/'):]}"
if os.path.isfile(docker_path):
return docker_path
try:
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
local_path = os.path.join(project_root, "picture", path[len('/picture/'):])
if os.path.isfile(local_path):
return local_path
except Exception:
pass
return None
return None
@staticmethod
def _get_http_headers(url: str) -> dict:
headers = {}
if "/api/v1/knowledge/" in url:
try:
from config import RAG_CONFIG
headers["x-user-id"] = str(RAG_CONFIG.get("x-user-id", "1"))
headers["x-user-name"] = str(RAG_CONFIG.get("x-user-name", "testuser"))
headers["x-role"] = str(RAG_CONFIG.get("x-role", "admin"))
except Exception:
headers["x-user-id"] = "1"
headers["x-user-name"] = "testuser"
headers["x-role"] = "admin"
return headers
def add_image(self, url_or_path: str, width_inches: float = 5.5):
if not url_or_path:
return
try:
import io
image_stream = None
if os.path.isfile(url_or_path):
image_stream = url_or_path
elif url_or_path.startswith("http://") or url_or_path.startswith("https://"):
local_path = self._convert_url_to_local_path(url_or_path)
if local_path and os.path.isfile(local_path):
image_stream = local_path
else:
import requests
headers = self._get_http_headers(url_or_path)
resp = requests.get(url_or_path, timeout=15, headers=headers, proxies={"http": None, "https": None})
resp.raise_for_status()
content_type = resp.headers.get("Content-Type", "")
logger.info(f"HTTP 下载完成, Content-Type: {content_type}, 大小: {len(resp.content)} bytes")
if not content_type.startswith("image/"):
logger.error(f"响应不是图片类型: {content_type}")
self.add_paragraph(f"[图片加载失败: {url_or_path}]")
return
image_stream = io.BytesIO(resp.content)
else:
logger.warning(f"图片路径无效: {url_or_path}")
return
from PIL import Image as PilImage
if isinstance(image_stream, str):
pil_img = PilImage.open(image_stream)
else:
pil_img = PilImage.open(image_stream)
png_stream = io.BytesIO()
pil_img.convert("RGBA" if pil_img.mode in ("RGBA", "P") else "RGB").save(png_stream, format="PNG")
png_stream.seek(0)
p = self.doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run()
run.add_picture(png_stream, width=Inches(width_inches))
logger.info(f"图片插入成功: {url_or_path}")
except Exception as e:
logger.error(f"插入图片失败 '{url_or_path}': {e!r}", exc_info=True)
self.add_paragraph(f"[图片加载失败: {url_or_path}]")
def _extract_image_urls(self, line_content):
"""
从一行文本中提取所有图片URL支持多种格式
- ![alt](url)
- ![alt]`url`
- ! `url`
返回 [(start, end, url), ...]
"""
results = []
# 格式1: ![alt](url)
for m in re.finditer(r'!\[([^\]]*)\]\(([^)]+)\)', line_content):
url = m.group(2).strip()
if url.startswith('`') and url.endswith('`'):
url = url[1:-1]
results.append((m.start(), m.end(), url))
# 格式2: ![alt]`url` (未被格式1匹配的)
for m in re.finditer(r'!\[([^\]]*)\]\s*`([^`]+)`', line_content):
url = m.group(2).strip()
already = any(s <= m.start() and e >= m.end() for s, e, _ in results)
if not already:
results.append((m.start(), m.end(), url))
# 格式3: ! `url` (模型常见的破损格式)
for m in re.finditer(r'!\s*`([^`]+\.(?:jpg|jpeg|png|gif|bmp|webp))`', line_content, re.IGNORECASE):
url = m.group(1).strip()
already = any(s <= m.start() and e >= m.end() for s, e, _ in results)
if not already:
results.append((m.start(), m.end(), url))
results.sort(key=lambda x: x[0])
return results
def add_table(self, lines):
try:
rows = []
for line in lines:
line = line.strip("|")
cells = [c.strip() for c in line.split("|")]
rows.append(cells)
if not rows:
return
header = rows[0]
data_start_index = 1
if len(rows) > 1 and all(c.startswith('-') or c == '' for c in rows[1]):
data_start_index = 2
data = rows[data_start_index:]
max_cols = len(header)
table = self.doc.add_table(rows=len(data) + 1, cols=max_cols)
table.style = "Table Grid"
for i, cell in enumerate(header):
if i < max_cols:
table.rows[0].cells[i].text = cell
for r, row in enumerate(data):
for c, val in enumerate(row):
if c < max_cols:
table.rows[r + 1].cells[c].text = val
logger.info("表格添加成功")
except Exception as e:
logger.error(f"添加表格失败: {e}")
def _add_horizontal_rule(self):
p = self.doc.add_paragraph()
pPr = p._element.get_or_add_pPr()
pBdr = p._element.makeelement(qn('w:pBdr'), {})
bottom = p._element.makeelement(qn('w:bottom'), {
qn('w:val'): 'single',
qn('w:sz'): '6',
qn('w:space'): '1',
qn('w:color'): 'CCCCCC',
})
pBdr.append(bottom)
pPr.append(pBdr)
def parse(self, markdown):
lines = markdown.split("\n")
table_buffer = []
block_formula = []
in_block_formula = False
i = 0
while i < len(lines):
line = lines[i]
line_stripped = line.rstrip()
line_content = line_stripped.lstrip()
# 1. 处理块级公式
if line_content.startswith("$$"):
if not in_block_formula:
remaining = line_content[2:]
close_idx = remaining.find("$$")
if close_idx >= 0:
formula = remaining[:close_idx].strip()
after_formula = remaining[close_idx + 2:].strip()
p = self.doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
if not (self._omml_available and self._add_omml_to_paragraph(p, formula)):
self.add_paragraph(f"[公式: {formula}]")
if after_formula:
self.add_paragraph(after_formula)
else:
in_block_formula = True
block_formula = []
if remaining.strip():
block_formula.append(remaining.strip())
else:
remaining = line_content[2:]
in_block_formula = False
formula = "".join(block_formula).strip()
p = self.doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
if not (self._omml_available and self._add_omml_to_paragraph(p, formula)):
self.add_paragraph(f"[公式: {formula}]")
if remaining.strip():
self.add_paragraph(remaining.strip())
i += 1
continue
if in_block_formula:
block_formula.append(line)
i += 1
continue
# 2. 处理表格
if line_content.startswith("|") and line_content.endswith("|"):
table_buffer.append(line_stripped)
i += 1
continue
else:
if table_buffer:
self.add_table(table_buffer)
table_buffer = []
# 空行跳过
if not line_stripped:
i += 1
continue
# 3. 水平分隔线
if re.match(r'^\s*[-*_]{3,}\s*$', line_stripped):
self._add_horizontal_rule()
i += 1
continue
# 4. 标题
heading_match = re.match(r'^(#{1,3})\s+(.*)', line_content)
if heading_match:
level = len(heading_match.group(1))
text = heading_match.group(2)
self.add_heading(text, level)
i += 1
continue
# 5. 有序列表(带子列表支持)
list_match_ordered = re.match(r'^(\d+)\.\s*(.*)', line_content)
if list_match_ordered:
number = int(list_match_ordered.group(1))
text = list_match_ordered.group(2)
self.add_list(text, numbered=True, number=number)
i += 1
while i < len(lines):
next_line = lines[i]
next_stripped = next_line.rstrip()
sub_match = re.match(r'^[\s\t]+[-*]\s*(.*)', next_stripped)
if sub_match:
sub_text = sub_match.group(1)
self.add_sub_list(sub_text)
i += 1
elif (next_stripped.startswith(' ') or next_stripped.startswith('\t')):
sub_content = next_stripped.strip()
if sub_content and not re.match(r'^\d+\.\s', sub_content):
self.add_sub_list(sub_content)
i += 1
else:
break
else:
break
continue
# 6. 无序列表
is_bold_line = line_content.startswith('**')
if not is_bold_line:
list_match_unordered = re.match(r'^\s*[-*]\s+(.*)', line_stripped)
if not list_match_unordered:
m = re.match(r'^\s*-([^-].*)$', line_stripped)
if m and m.group(1).strip():
list_match_unordered = m
if list_match_unordered:
text = list_match_unordered.group(1).strip()
if text:
self.add_list(text, numbered=False)
i += 1
continue
# 7. 图片链接 - 统一提取所有格式的图片URL
img_urls = self._extract_image_urls(line_content)
if img_urls:
has_only_img = (len(img_urls) == 1 and
line_content.strip().startswith('!') and
img_urls[0][0] == 0)
if has_only_img:
_, _, url = img_urls[0]
self.add_image(url)
else:
p = self.doc.add_paragraph()
last_end = 0
for start, end, url in img_urls:
before_text = line_content[last_end:start].strip()
if before_text:
self._add_rich_text_with_formulas(p, before_text)
self.add_image(url, width_inches=4.5)
p = self.doc.add_paragraph()
last_end = end
after_text = line_content[last_end:].strip()
if after_text:
self._add_rich_text_with_formulas(p, after_text)
i += 1
continue
# 8. 普通链接
if line_content.startswith("["):
match = re.match(r'\[(.*?)\]\((.*?)\)', line_content)
if match:
p = self.doc.add_paragraph()
run = p.add_run(match.group(1))
run.font.color.rgb = RGBColor(0, 0, 255)
run.font.underline = True
i += 1
continue
# 9. 普通段落
p = self.doc.add_paragraph()
indent_chars = 0
temp_line = line_stripped
while temp_line.startswith('\u3000'):
indent_chars += 1
temp_line = temp_line[1:]
if indent_chars >= 2:
p.paragraph_format.first_line_indent = Inches(0.5)
elif indent_chars == 1:
p.paragraph_format.first_line_indent = Inches(0.25)
self._add_rich_text_with_formulas(p, line_content)
i += 1
if table_buffer:
self.add_table(table_buffer)
def generate(self, markdown, title=None, output=None, images=None):
try:
if title:
logger.info(f"添加文档标题: {title}")
p = self.doc.add_paragraph()
run = p.add_run(title)
self.set_font(run, 20, True)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
logger.info("开始解析 Markdown 内容")
self.parse(markdown)
if images:
self._append_images_section(images)
if not output:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output = f"report_{timestamp}.docx"
logger.info(f"正在保存文件: {output}")
self.doc.save(output)
if os.path.exists(output):
file_size = os.path.getsize(output)
logger.info(f"文件保存成功: {output}, 大小: {file_size} bytes")
return output
else:
logger.error(f"文件保存后未找到: {output}")
raise FileNotFoundError(f"File {output} was not created.")
except Exception as e:
logger.error(f"生成报告过程中发生严重错误: {e}", exc_info=True)
raise
def _append_images_section(self, images):
try:
self.add_heading("附件图片", level=2)
for idx, img in enumerate(images, 1):
if isinstance(img, dict):
url = img.get('url', '')
summary = img.get('summary', '')
else:
url = str(img)
summary = ''
p = self.doc.add_paragraph()
run = p.add_run(f"图片{idx}")
self.set_font(run, bold=True)
if url:
self.add_image(url, width_inches=4)
if summary:
p = self.doc.add_paragraph()
run = p.add_run(f"说明:{summary}")
self.set_font(run, size=10)
self.doc.add_paragraph()
logger.info(f"图片附件部分添加完成,共 {len(images)} 张图片")
except Exception as e:
logger.error(f"添加图片附件部分失败: {e}")
def generate_report_word(markdown, title=None, output=None, images=None):
logger.info("=== 开始生成报告 ===")
try:
generator = MarkdownWordGenerator()
if images:
logger.info(f"准备插入 {len(images)} 张图片")
file_path = generator.generate(markdown, title=title, output=output, images=images)
logger.info(f"=== 报告生成完成: {file_path} ===")
return file_path
except Exception as e:
logger.critical(f"报告生成失败: {e}")
raise
if __name__ == "__main__":
md = r"""
## **测试报告 - OMML公式和图片**
### **一、行内公式**
温度要求 $ \leqslant 40 ^ { \circ } \mathrm { C } $不得超过
公差范围 $ \geqslant 0.05 \ \mathrm { mm } $ 以内合格
### **二、列表中的公式**
1. 温度 $ \leqslant 40 ^ { \circ } \mathrm { C } $ 要求
2. 间隙 $ \leqslant 0.05 \ \mathrm { mm } $ 标准
- 清洁轴颈 $ \geqslant 0.05 \ \mathrm { mm } $
### **四、图片测试**
![图片](http://192.168.0.46:11000/api/v1/knowledge/files/images/56b1c0a02ff03d9d202627e88603e6671d3f193511d98a9fab0daf8687e6a9fa.jpg)
"""
try:
result_path = generate_report_word(md)
print(f"报告已生成,路径: {result_path}")
except Exception as e:
print(f"程序执行出错: {e}")
import traceback
traceback.print_exc()

View File

@ -26,7 +26,7 @@ WORKFLOW_CONFIG = {
"function": "run_qa_workflow", "function": "run_qa_workflow",
"is_async": True, "is_async": True,
}, },
"故障排查与修理": { "问题排查": {
"module": "workflows.workflow_fault_diagnosis", "module": "workflows.workflow_fault_diagnosis",
"function": "run_fault_diagnosis_workflow", "function": "run_fault_diagnosis_workflow",
"is_async": True, "is_async": True,
@ -58,7 +58,7 @@ WORKFLOW_CONFIG = {
"function": "run_fault_diagnosis_workflow", "function": "run_fault_diagnosis_workflow",
"is_async": True, "is_async": True,
}, },
"舰船百科":{ "百科问答":{
"module": "workflows.workflow_baike", "module": "workflows.workflow_baike",
"function": "run_baike_workflow", "function": "run_baike_workflow",
"is_async": True, "is_async": True,

0
workflows/=0.2.0 Normal file
View File

55
workflows/=2.0.0 Normal file
View File

@ -0,0 +1,55 @@
Requirement already satisfied: langgraph in /usr/local/lib/python3.10/dist-packages (1.0.3)
Collecting langgraph
Downloading langgraph-1.1.3-py3-none-any.whl (168 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 168.1/168.1 KB 685.8 kB/s eta 0:00:00
Requirement already satisfied: langgraph-checkpoint-postgres in /usr/local/lib/python3.10/dist-packages (3.0.5)
Requirement already satisfied: langchain-core>=0.1 in /usr/local/lib/python3.10/dist-packages (from langgraph) (1.1.0)
Collecting langgraph-sdk<0.4.0,>=0.3.0
Downloading langgraph_sdk-0.3.12-py3-none-any.whl (95 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 95.8/95.8 KB 3.9 MB/s eta 0:00:00
Requirement already satisfied: xxhash>=3.5.0 in /usr/local/lib/python3.10/dist-packages (from langgraph) (3.6.0)
Requirement already satisfied: pydantic>=2.7.4 in /usr/local/lib/python3.10/dist-packages (from langgraph) (2.12.4)
Requirement already satisfied: langgraph-checkpoint<5.0.0,>=2.1.0 in /usr/local/lib/python3.10/dist-packages (from langgraph) (3.0.1)
Collecting langgraph-prebuilt<1.1.0,>=1.0.8
Downloading langgraph_prebuilt-1.0.8-py3-none-any.whl (35 kB)
Requirement already satisfied: psycopg-pool>=3.2.0 in /usr/local/lib/python3.10/dist-packages (from langgraph-checkpoint-postgres) (3.3.0)
Requirement already satisfied: orjson>=3.11.5 in /usr/local/lib/python3.10/dist-packages (from langgraph-checkpoint-postgres) (3.11.7)
Requirement already satisfied: psycopg>=3.2.0 in /usr/local/lib/python3.10/dist-packages (from langgraph-checkpoint-postgres) (3.3.3)
Requirement already satisfied: jsonpatch<2.0.0,>=1.33.0 in /usr/local/lib/python3.10/dist-packages (from langchain-core>=0.1->langgraph) (1.33)
Requirement already satisfied: langsmith<1.0.0,>=0.3.45 in /usr/local/lib/python3.10/dist-packages (from langchain-core>=0.1->langgraph) (0.4.46)
Requirement already satisfied: packaging<26.0.0,>=23.2.0 in /usr/local/lib/python3.10/dist-packages (from langchain-core>=0.1->langgraph) (25.0)
Requirement already satisfied: pyyaml<7.0.0,>=5.3.0 in /usr/local/lib/python3.10/dist-packages (from langchain-core>=0.1->langgraph) (6.0.3)
Requirement already satisfied: tenacity!=8.4.0,<10.0.0,>=8.1.0 in /usr/local/lib/python3.10/dist-packages (from langchain-core>=0.1->langgraph) (9.1.2)
Requirement already satisfied: typing-extensions<5.0.0,>=4.7.0 in /usr/local/lib/python3.10/dist-packages (from langchain-core>=0.1->langgraph) (4.15.0)
Requirement already satisfied: ormsgpack>=1.12.0 in /usr/local/lib/python3.10/dist-packages (from langgraph-checkpoint<5.0.0,>=2.1.0->langgraph) (1.12.0)
Requirement already satisfied: httpx>=0.25.2 in /usr/local/lib/python3.10/dist-packages (from langgraph-sdk<0.4.0,>=0.3.0->langgraph) (0.28.1)
Requirement already satisfied: typing-inspection>=0.4.2 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2.7.4->langgraph) (0.4.2)
Requirement already satisfied: annotated-types>=0.6.0 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2.7.4->langgraph) (0.7.0)
Requirement already satisfied: pydantic-core==2.41.5 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2.7.4->langgraph) (2.41.5)
Requirement already satisfied: httpcore==1.* in /usr/local/lib/python3.10/dist-packages (from httpx>=0.25.2->langgraph-sdk<0.4.0,>=0.3.0->langgraph) (1.0.9)
Requirement already satisfied: anyio in /usr/local/lib/python3.10/dist-packages (from httpx>=0.25.2->langgraph-sdk<0.4.0,>=0.3.0->langgraph) (4.11.0)
Requirement already satisfied: certifi in /usr/local/lib/python3.10/dist-packages (from httpx>=0.25.2->langgraph-sdk<0.4.0,>=0.3.0->langgraph) (2025.11.12)
Requirement already satisfied: idna in /usr/local/lib/python3.10/dist-packages (from httpx>=0.25.2->langgraph-sdk<0.4.0,>=0.3.0->langgraph) (3.11)
Requirement already satisfied: h11>=0.16 in /usr/local/lib/python3.10/dist-packages (from httpcore==1.*->httpx>=0.25.2->langgraph-sdk<0.4.0,>=0.3.0->langgraph) (0.16.0)
Requirement already satisfied: jsonpointer>=1.9 in /usr/local/lib/python3.10/dist-packages (from jsonpatch<2.0.0,>=1.33.0->langchain-core>=0.1->langgraph) (3.0.0)
Requirement already satisfied: requests>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from langsmith<1.0.0,>=0.3.45->langchain-core>=0.1->langgraph) (2.32.5)
Requirement already satisfied: zstandard>=0.23.0 in /usr/local/lib/python3.10/dist-packages (from langsmith<1.0.0,>=0.3.45->langchain-core>=0.1->langgraph) (0.25.0)
Requirement already satisfied: requests-toolbelt>=1.0.0 in /usr/local/lib/python3.10/dist-packages (from langsmith<1.0.0,>=0.3.45->langchain-core>=0.1->langgraph) (1.0.0)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests>=2.0.0->langsmith<1.0.0,>=0.3.45->langchain-core>=0.1->langgraph) (2.5.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests>=2.0.0->langsmith<1.0.0,>=0.3.45->langchain-core>=0.1->langgraph) (3.4.4)
Requirement already satisfied: exceptiongroup>=1.0.2 in /usr/local/lib/python3.10/dist-packages (from anyio->httpx>=0.25.2->langgraph-sdk<0.4.0,>=0.3.0->langgraph) (1.3.1)
Requirement already satisfied: sniffio>=1.1 in /usr/local/lib/python3.10/dist-packages (from anyio->httpx>=0.25.2->langgraph-sdk<0.4.0,>=0.3.0->langgraph) (1.3.1)
Installing collected packages: langgraph-sdk, langgraph-prebuilt, langgraph
Attempting uninstall: langgraph-sdk
Found existing installation: langgraph-sdk 0.2.9
Uninstalling langgraph-sdk-0.2.9:
Successfully uninstalled langgraph-sdk-0.2.9
Attempting uninstall: langgraph-prebuilt
Found existing installation: langgraph-prebuilt 1.0.5
Uninstalling langgraph-prebuilt-1.0.5:
Successfully uninstalled langgraph-prebuilt-1.0.5
Attempting uninstall: langgraph
Found existing installation: langgraph 1.0.3
Uninstalling langgraph-1.0.3:
Successfully uninstalled langgraph-1.0.3
Successfully installed langgraph-1.1.3 langgraph-prebuilt-1.0.8 langgraph-sdk-0.3.12

View File

@ -220,8 +220,7 @@ async def search_rag_and_graph(state: QAState) -> Dict[str, Any]:
}) })
# 只保留前 4 条数据 # 只保留前 4 条数据
# rag_search_result = rag_search_result[:2] rag_search_result = rag_search_result[:2]
rag_search_result = sorted(rag_search_result,key=lambda x:float(x.get("socre") or 0), reverse=True)[:2]
return { return {
"rag_search_result": rag_search_result, "rag_search_result": rag_search_result,
@ -301,8 +300,7 @@ async def generate_answer(state: QAState) -> Dict[str, Any]:
if need_retrieval: if need_retrieval:
# 收集所有 RAG 结果文本(只取前 4 条) # 收集所有 RAG 结果文本(只取前 4 条)
for item in sorted(rag_results or [], key=lambda x: float(x.get("score") or 0) if isinstance(x,dict) else 0, reverse=True)[:2]: for item in (rag_results or [])[:1]:
# for item in (rag_results or [])[:1]:
print(item) print(item)
print(111111111111111111111111111) print(111111111111111111111111111)
if isinstance(item, dict): if isinstance(item, dict):

View File

@ -4,9 +4,9 @@
from typing import TypedDict, Optional, Dict, Any, List from typing import TypedDict, Optional, Dict, Any, List
from langgraph.graph import StateGraph, START, END from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command from langgraph.types import interrupt, Command
from tools.function_tool import graph_rag_search, fault_graph_rag_search from tools.function_tool import graph_rag_search
from tools.rag_tools import rag_search from tools.rag_tools import rag_search
from tools.graph_tools import atlas_retrieval, format_atlas_results, extract_entity_names_from_history, normalize_device_name_by_ship_graph from tools.graph_tools import atlas_retrieval, format_atlas_results, extract_entity_names_from_history
from modelsAPI.model_api import OpenaiAPI from modelsAPI.model_api import OpenaiAPI
from utils.function_tracker import get_all_callbacks from utils.function_tracker import get_all_callbacks
from utils.image_utils import extract_image_paths, normalize_markdown_images, get_image_prompt_guidance from utils.image_utils import extract_image_paths, normalize_markdown_images, get_image_prompt_guidance
@ -720,95 +720,66 @@ async def call_tools(state: FaultDiagnosisState) -> Dict[str, Any]:
matched_kb_id = "" matched_kb_id = ""
mapped_ship_number = "" mapped_ship_number = ""
if device_name:
normalized_result = await normalize_device_name_by_ship_graph(device_name)
if normalized_result.get("success"):
normalized_device_name = normalized_result.get("device_name") or device_name
if normalized_device_name != device_name:
print(
f"[设备标准化] '{device_name}' -> '{normalized_device_name}', "
f"score={normalized_result.get('score')}"
)
device_name = normalized_device_name
else:
print(f"[设备标准化] 使用原始设备名称 '{device_name}': {normalized_result.get('error')}")
async def _do_rag_search(): async def _do_rag_search():
from utils.ship_number_search import search_with_ship_number_strategy from utils.ship_number_search import search_with_ship_number_strategy
base_query = f"{device_name}{fault}该怎么修复"
base_query = f"设备{device_name},故障{fault}的维修方案" results, kb_name, kb_id, mapped_num = await search_with_ship_number_strategy(
results, kb_name, kb_id, mapped_number = await search_with_ship_number_strategy( base_query=base_query, ship_number=ship_number, top_k=5, search_type="fault"
base_query=base_query,
ship_number=ship_number,
top_k=5,
search_type="fault",
) )
if not kb_name: if not kb_name:
kb_name = "通用知识库" kb_name = "通用知识库"
print(f"RAG 检索来源: {kb_name}") print(f"RAG 检索来源: {kb_name}")
return results, kb_name, kb_id, mapped_number return results, kb_name, kb_id, mapped_num
async def _do_graph_search(): async def _do_graph_search():
if not (device_name and fault): if not (device_name and fault):
return "" return ""
try: try:
results = await fault_graph_rag_search.ainvoke( graph_query = f"{device_name}{fault}该怎么修复"
{"device_name": device_name, "fault_symptom": fault, "ship_number": ""} results = await graph_rag_search.ainvoke({"query": graph_query})
) if len(results) < 60:
graph_query = f"{fault}该怎么修复"
results = await graph_rag_search.ainvoke({"query": graph_query})
print(f"图谱检索完成,结果长度: {len(results) if results else 0}") print(f"图谱检索完成,结果长度: {len(results) if results else 0}")
return results return results
except Exception as e: except Exception as e:
print(f"图谱检索失败: {str(e)}") print(f"图谱检索失败: {str(e)}")
return "" return ""
async def _do_atlas_search(): need_rag = "rag_search" in tools_to_call and rag_results is None
if not device_name: need_graph = "graph_rag_search" in tools_to_call and graph_results is None
return {}
try:
print(f"[图册检索] 使用设备名称: {device_name}")
result = await atlas_retrieval(node_names=[device_name], top_k=10)
if result.get("success", False):
data = result.get("data", {})
print(f"[图册检索] 图册检索结果: {list(data.keys())}")
return data
except Exception as e:
print(f"[图册检索] 图册检索失败: {str(e)}")
return {}
tasks = [] if need_rag and need_graph:
task_names = [] (rag_results, matched_kb_name, matched_kb_id, mapped_ship_number), graph_results = \
if "rag_search" in tools_to_call and rag_results is None: await asyncio.gather(_do_rag_search(), _do_graph_search())
tasks.append(_do_rag_search()) elif need_rag:
task_names.append("rag") rag_results, matched_kb_name, matched_kb_id, mapped_ship_number = await _do_rag_search()
if "graph_rag_search" in tools_to_call and graph_results is None: elif need_graph:
tasks.append(_do_graph_search()) graph_results = await _do_graph_search()
task_names.append("graph")
if device_name:
tasks.append(_do_atlas_search())
task_names.append("atlas")
if tasks:
task_results = await asyncio.gather(*tasks)
for task_name, task_result in zip(task_names, task_results):
if task_name == "rag":
rag_results, matched_kb_name, matched_kb_id, mapped_ship_number = task_result
elif task_name == "graph":
graph_results = task_result
elif task_name == "atlas":
atlas_results = task_result
if mapped_ship_number and mapped_ship_number != ship_number: if mapped_ship_number and mapped_ship_number != ship_number:
print(f"[call_tools] 舷号映射: '{ship_number}' -> '{mapped_ship_number}',更新工作流状态") print(f"[call_tools] 舷号映射: '{ship_number}' -> '{mapped_ship_number}',更新工作流状态")
ship_number = mapped_ship_number ship_number = mapped_ship_number
# 图册检索:只传入设备名称
if device_name:
try:
print(f"[图册检索] 使用设备名称: {device_name}")
atlas_result = await atlas_retrieval(node_names=[device_name], top_k=10)
if atlas_result.get("success", False):
atlas_results = atlas_result.get("data", {})
print(f"[图册检索] 图册检索结果: {list(atlas_results.keys())}")
except Exception as e:
print(f"[图册检索] 图册检索失败: {str(e)}")
has_rag = bool(rag_results and len(rag_results) > 0) has_rag = bool(rag_results and len(rag_results) > 0)
has_graph = bool(graph_results and len(graph_results) > 100) has_graph = bool(graph_results and len(graph_results) > 100)
if not has_rag and not has_graph: if not has_rag and not has_graph:
print(f"[call_tools] 未找到任何相关资料,需要用户确认使用模型能力") print(f"[call_tools] 未找到任何相关资料,需要用户确认使用模型能力")
return {"rag_search_result": rag_results if isinstance(rag_results, list) else [],"graph_search_result": graph_results if isinstance(graph_results, str) else "","atlas_results": atlas_results,"matched_kb_id": matched_kb_id,"matched_kb_name": matched_kb_name,"ship_number": ship_number,"device_name": device_name,"has_rag_result": False,"has_graph_result": False,"need_model_confirm": True,"current_node": "model_confirm",} return {"rag_search_result": rag_results if isinstance(rag_results, list) else [],"graph_search_result": graph_results if isinstance(graph_results, str) else "","atlas_results": atlas_results,"matched_kb_id": matched_kb_id,"matched_kb_name": matched_kb_name,"ship_number": ship_number,"has_rag_result": False,"has_graph_result": False,"need_model_confirm": True,"current_node": "model_confirm",}
return {"rag_search_result": rag_results,"graph_search_result": graph_results,"atlas_results": atlas_results,"matched_kb_id": matched_kb_id,"matched_kb_name": matched_kb_name,"ship_number": ship_number,"device_name": device_name,"has_rag_result": has_rag,"has_graph_result": has_graph,"need_model_confirm": False,"current_node": "generate_response",} return {"rag_search_result": rag_results,"graph_search_result": graph_results,"atlas_results": atlas_results,"matched_kb_id": matched_kb_id,"matched_kb_name": matched_kb_name,"ship_number": ship_number,"has_rag_result": has_rag,"has_graph_result": has_graph,"need_model_confirm": False,"current_node": "generate_response",}
async def model_confirm(state: FaultDiagnosisState) -> Command: async def model_confirm(state: FaultDiagnosisState) -> Command:
""" """
@ -1689,3 +1660,5 @@ async def run_fault_diagnosis_workflow(extracted_text: str,combined_query: str,h
route_flag = "fault_diagnosis" route_flag = "fault_diagnosis"
return {"response": result.get("response", ""),"actions": result.get("actions", []),"result_tag": route_flag,"suggestedReplies": result.get("suggestedReplies", []),"route_flag": route_flag,} return {"response": result.get("response", ""),"actions": result.get("actions", []),"result_tag": route_flag,"suggestedReplies": result.get("suggestedReplies", []),"route_flag": route_flag,}

View File

@ -4,7 +4,7 @@
from typing import TypedDict, Optional, Dict, Any, List from typing import TypedDict, Optional, Dict, Any, List
from langgraph.graph import StateGraph, START, END from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command from langgraph.types import interrupt, Command
from tools.function_tool import graph_rag_search from tools.function_tool import graph_rag_search, fault_graph_rag_search
from tools.rag_tools import rag_search from tools.rag_tools import rag_search
from tools.graph_tools import atlas_retrieval, format_atlas_results, extract_entity_names_from_history from tools.graph_tools import atlas_retrieval, format_atlas_results, extract_entity_names_from_history
from modelsAPI.model_api import OpenaiAPI from modelsAPI.model_api import OpenaiAPI
@ -720,47 +720,31 @@ async def call_tools(state: FaultDiagnosisState) -> Dict[str, Any]:
matched_kb_id = "" matched_kb_id = ""
mapped_ship_number = "" mapped_ship_number = ""
async def _do_rag_search(): if "rag_search" in tools_to_call and rag_results is None:
from utils.ship_number_search import search_with_ship_number_strategy from utils.ship_number_search import search_with_ship_number_strategy
base_query = f"{device_name}中{fault}该怎么修复"
results, kb_name, kb_id, mapped_num = await search_with_ship_number_strategy(
base_query=base_query, ship_number=ship_number, top_k=5, search_type="fault"
)
if not kb_name:
kb_name = "通用知识库"
print(f"RAG 检索来源: {kb_name}")
return results, kb_name, kb_id, mapped_num
async def _do_graph_search(): base_query = f"设备{device_name},故障{fault}的维修方案"
if not (device_name and fault):
return ""
try:
graph_query = f"{device_name}中{fault}该怎么修复"
results = await graph_rag_search.ainvoke({"query": graph_query})
if len(results) < 60:
graph_query = f"{fault}该怎么修复"
results = await graph_rag_search.ainvoke({"query": graph_query})
print(f"图谱检索完成,结果长度: {len(results) if results else 0}")
return results
except Exception as e:
print(f"图谱检索失败: {str(e)}")
return ""
need_rag = "rag_search" in tools_to_call and rag_results is None rag_results, matched_kb_name, matched_kb_id, mapped_ship_number = await search_with_ship_number_strategy(base_query=base_query,ship_number=ship_number,top_k=5,search_type="fault")
need_graph = "graph_rag_search" in tools_to_call and graph_results is None
if need_rag and need_graph: if not matched_kb_name:
(rag_results, matched_kb_name, matched_kb_id, mapped_ship_number), graph_results = \ matched_kb_name = "通用知识库"
await asyncio.gather(_do_rag_search(), _do_graph_search())
elif need_rag: print(f"RAG 检索来源: {matched_kb_name}")
rag_results, matched_kb_name, matched_kb_id, mapped_ship_number = await _do_rag_search()
elif need_graph:
graph_results = await _do_graph_search()
if mapped_ship_number and mapped_ship_number != ship_number: if mapped_ship_number and mapped_ship_number != ship_number:
print(f"[call_tools] 舷号映射: '{ship_number}' -> '{mapped_ship_number}',更新工作流状态") print(f"[call_tools] 舷号映射: '{ship_number}' -> '{mapped_ship_number}',更新工作流状态")
ship_number = mapped_ship_number ship_number = mapped_ship_number
if "graph_rag_search" in tools_to_call and graph_results is None:
if device_name and fault:
try:
graph_results = await fault_graph_rag_search.ainvoke({"device_name": device_name, "fault_symptom": fault, "ship_number": ship_number or ""})
print(f"图谱检索完成,结果长度: {len(graph_results) if graph_results else 0}")
except Exception as e:
print(f"图谱检索失败: {str(e)}")
graph_results = ""
# 图册检索:只传入设备名称 # 图册检索:只传入设备名称
if device_name: if device_name:
try: try:
@ -876,8 +860,7 @@ async def generate_response(state: FaultDiagnosisState) -> Dict[str, Any]:
if isinstance(rag_results, str) and rag_results.strip(): if isinstance(rag_results, str) and rag_results.strip():
rag_ctx_parts = [rag_results.strip()] rag_ctx_parts = [rag_results.strip()]
elif isinstance(rag_results, list): elif isinstance(rag_results, list):
for item in sorted(rag_results, key=lambda x: float(x.get("score") or 0) if isinstance(x, dict) else 0, reverse=True)[:2]: for item in rag_results[:1]:
# for item in rag_results[:1]:
if isinstance(item, dict): if isinstance(item, dict):
text = (item.get("text") or "").strip() text = (item.get("text") or "").strip()
if text: if text:
@ -899,7 +882,7 @@ async def generate_response(state: FaultDiagnosisState) -> Dict[str, Any]:
rag_answer = graph_results rag_answer = graph_results
rag_type = "graph" rag_type = "graph"
elif rag_ctx_parts: elif rag_ctx_parts:
rag_answer = "\n\n".join(rag_ctx_parts[:2]) rag_answer = "\n\n".join(rag_ctx_parts[:1])
rag_type = "rag" rag_type = "rag"
if not rag_answer or rag_answer == "NO_RELEVANT_RESULT": if not rag_answer or rag_answer == "NO_RELEVANT_RESULT":

View File

@ -4,7 +4,7 @@
from typing import TypedDict, Optional, Dict, Any, List from typing import TypedDict, Optional, Dict, Any, List
from langgraph.graph import StateGraph, START, END from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command from langgraph.types import interrupt, Command
from tools.function_tool import graph_rag_search, operation_graph_rag_search from tools.function_tool import graph_rag_search
from tools.rag_tools import rag_search from tools.rag_tools import rag_search
from tools.graph_tools import atlas_retrieval, format_atlas_results, extract_entity_names_from_history from tools.graph_tools import atlas_retrieval, format_atlas_results, extract_entity_names_from_history
from modelsAPI.model_api import OpenaiAPI from modelsAPI.model_api import OpenaiAPI
@ -153,7 +153,7 @@ async def extract_info(state: OperateGuideState) -> Dict[str, Any]:
combined_query = state.get("combined_query", "") or "" combined_query = state.get("combined_query", "") or ""
history_messages = state.get("history_messages", []) or [] history_messages = state.get("history_messages", []) or []
history_str = build_history_str(history_messages, recent_count=10) history_str = build_history_str(history_messages, recent_count=4)
existing_info = [] existing_info = []
existing_ship_number = state.get("ship_number", "") or "" existing_ship_number = state.get("ship_number", "") or ""
@ -270,7 +270,7 @@ async def agent_think(state: OperateGuideState) -> Dict[str, Any]:
tools_to_call.append("graph_rag_search") tools_to_call.append("graph_rag_search")
return {"scheme_generated": False,"rag_search_result": None,"graph_search_result": None,"deep_rag_results": None,"has_rag_result": False,"has_graph_result": False,"is_regenerating": False,"user_feedback_for_regenerate": "","follow_up_intent": "","user_confirmed_info": True,"initialized": True,"iteration_count": 0,"ask_round": 0,"agent_decision": "call_tools","tools_to_call": tools_to_call,"current_node": "call_tools",} return {"scheme_generated": False,"rag_search_result": None,"graph_search_result": None,"deep_rag_results": None,"has_rag_result": False,"has_graph_result": False,"is_regenerating": False,"user_feedback_for_regenerate": "","follow_up_intent": "","user_confirmed_info": True,"initialized": True,"iteration_count": 0,"ask_round": 0,"agent_decision": "call_tools","tools_to_call": tools_to_call,"current_node": "call_tools",}
history_str = build_history_str(history_messages, recent_count=8, assistant_truncate=300) history_str = build_history_str(history_messages, recent_count=5, assistant_truncate=300)
intent_system_prompt = OPERATE_PROMPTS["agent_think_intent_system"] intent_system_prompt = OPERATE_PROMPTS["agent_think_intent_system"]
@ -591,7 +591,7 @@ async def call_tools(state: OperateGuideState) -> Dict[str, Any]:
async def _do_rag_search(): async def _do_rag_search():
from utils.ship_number_search import search_with_ship_number_strategy from utils.ship_number_search import search_with_ship_number_strategy
base_query = f"设备{device_name},操作{operation_item}的操作指导方案" base_query = f"如何执行{device_name}{operation_item}操作"
results, kb_name, kb_id, _ = await search_with_ship_number_strategy( results, kb_name, kb_id, _ = await search_with_ship_number_strategy(
base_query=base_query, ship_number=ship_number, top_k=5, search_type="operate" base_query=base_query, ship_number=ship_number, top_k=5, search_type="operate"
) )
@ -604,9 +604,8 @@ async def call_tools(state: OperateGuideState) -> Dict[str, Any]:
if not (device_name and operation_item): if not (device_name and operation_item):
return "" return ""
try: try:
graph_query = f"设备{device_name},操作{operation_item}的操作指导方案" graph_query = f"如何执行{device_name}{operation_item}操作"
results = await operation_graph_rag_search.ainvoke({"ship_number": 163, "device_name": device_name, "operation_item": operation_item}) results = await graph_rag_search.ainvoke({"query": graph_query})
print(f"图谱检索完成,结果长度: {len(results) if results else 0}") print(f"图谱检索完成,结果长度: {len(results) if results else 0}")
return results return results
except Exception as e: except Exception as e:
@ -617,10 +616,8 @@ async def call_tools(state: OperateGuideState) -> Dict[str, Any]:
need_graph = "graph_rag_search" in tools_to_call and graph_results is None need_graph = "graph_rag_search" in tools_to_call and graph_results is None
if need_rag and need_graph: if need_rag and need_graph:
(rag_results, matched_kb_name, matched_kb_id), graph_results = await asyncio.gather( (rag_results, matched_kb_name, matched_kb_id), graph_results = \
_do_rag_search(), await asyncio.gather(_do_rag_search(), _do_graph_search())
_do_graph_search()
)
elif need_rag: elif need_rag:
rag_results, matched_kb_name, matched_kb_id = await _do_rag_search() rag_results, matched_kb_name, matched_kb_id = await _do_rag_search()
elif need_graph: elif need_graph:
@ -708,7 +705,7 @@ async def generate_response(state: OperateGuideState) -> Dict[str, Any]:
matched_kb_name = state.get("matched_kb_name", "") matched_kb_name = state.get("matched_kb_name", "")
print("rag_results", rag_results) print("rag_results", rag_results)
history_str = build_history_str(history_messages, recent_count=12, assistant_truncate=500) history_str = build_history_str(history_messages, recent_count=5, assistant_truncate=500)
has_rag = rag_results and len(rag_results) > 0 has_rag = rag_results and len(rag_results) > 0
has_graph = graph_results and len(graph_results) > 100 has_graph = graph_results and len(graph_results) > 100
@ -741,7 +738,7 @@ async def generate_response(state: OperateGuideState) -> Dict[str, Any]:
if isinstance(rag_results, str) and rag_results.strip(): if isinstance(rag_results, str) and rag_results.strip():
rag_ctx_parts = [rag_results.strip()] rag_ctx_parts = [rag_results.strip()]
elif isinstance(rag_results, list): elif isinstance(rag_results, list):
for item in sorted(rag_results, key=lambda x: float(x.get("score") or 0) if isinstance(x, dict) else 0, reverse=True)[:2]: for item in rag_results[:8]:
if isinstance(item, dict): if isinstance(item, dict):
text = (item.get("text") or "").strip() text = (item.get("text") or "").strip()
if text: if text:
@ -763,7 +760,7 @@ async def generate_response(state: OperateGuideState) -> Dict[str, Any]:
rag_answer = graph_results rag_answer = graph_results
rag_type = "graph" rag_type = "graph"
elif rag_ctx_parts: elif rag_ctx_parts:
rag_answer = "\n\n".join(rag_ctx_parts[:2]) rag_answer = "\n\n".join(rag_ctx_parts[:3])
rag_type = "rag" rag_type = "rag"
if not rag_answer or rag_answer == "NO_RELEVANT_RESULT": if not rag_answer or rag_answer == "NO_RELEVANT_RESULT":
@ -1106,7 +1103,7 @@ async def deep_rag_search(state: OperateGuideState) -> Dict[str, Any]:
# ========== 第一步:分析之前方案和用户问题,选择深度分析点 ========== # ========== 第一步:分析之前方案和用户问题,选择深度分析点 ==========
if last_generated_scheme or combined_query: if last_generated_scheme or combined_query:
try: try:
history_str = build_history_str(history_messages, recent_count=8, assistant_truncate=300) history_str = build_history_str(history_messages, recent_count=4, assistant_truncate=300)
detail_info = "" detail_info = ""
if operation_code: if operation_code:
@ -1221,7 +1218,7 @@ async def generate_deep_response(state: OperateGuideState) -> Dict[str, Any]:
results = deep_rag_results.get(key, []) results = deep_rag_results.get(key, [])
if results: if results:
analysis_points_text += f"\n【分析点:{point}\n" analysis_points_text += f"\n【分析点:{point}\n"
for i, item in enumerate(sorted(results, key=lambda x: float(x.get("score") or 0) if isinstance(x, dict) else 0, reverse=True)[:2], 1): for i, item in enumerate(results[:2], 1):
if isinstance(item, dict): if isinstance(item, dict):
text = item.get("text", "") text = item.get("text", "")
if text: if text:
@ -1231,7 +1228,7 @@ async def generate_deep_response(state: OperateGuideState) -> Dict[str, Any]:
original_rag_text = "" original_rag_text = ""
if original_rag_results: if original_rag_results:
original_rag_text = "\n【原始检索资料】\n" original_rag_text = "\n【原始检索资料】\n"
for i, item in enumerate(sorted(original_rag_results, key=lambda x: float(x.get("score") or 0) if isinstance(x, dict) else 0, reverse=True)[:2], 1): for i, item in enumerate(original_rag_results[:3], 1):
if isinstance(item, dict): if isinstance(item, dict):
text = item.get("text", "") text = item.get("text", "")
if text: if text:
@ -1468,3 +1465,5 @@ async def run_operate_workflow(extracted_text: str,combined_query: str,history_m
route_flag = "operate" route_flag = "operate"
return {"response": result.get("response", ""),"actions": result.get("actions", []),"result_tag": route_flag,"suggestedReplies": result.get("suggestedReplies", []),"route_flag": route_flag,} return {"response": result.get("response", ""),"actions": result.get("actions", []),"result_tag": route_flag,"suggestedReplies": result.get("suggestedReplies", []),"route_flag": route_flag,}

File diff suppressed because it is too large Load Diff