Commit 15c33bcb authored by uuo00_n's avatar uuo00_n

feat(llm-service): 添加Dify智能体内容安全检查功能

在对话服务中集成Dify智能体进行二次内容安全检查,当本地敏感词过滤通过后,会调用Dify API进行更智能的内容安全检测。同时更新相关配置和文档描述,移除不再使用的仪表盘功能。

- 在docker-compose和配置中添加Dify相关环境变量
- 实现Dify服务调用和结果解析逻辑
- 在对话处理流程中集成Dify检查
- 移除LLM服务中不再使用的仪表盘相关代码
parent 40f9fb59
...@@ -76,6 +76,9 @@ services: ...@@ -76,6 +76,9 @@ services:
- TERM_START_DATE=2025-09-01 - TERM_START_DATE=2025-09-01
- OLLAMA_BASE_URL=http://datacenter.dldzxx.cn:11434/ - OLLAMA_BASE_URL=http://datacenter.dldzxx.cn:11434/
- OLLAMA_MODEL=deepseek-r1:14b - OLLAMA_MODEL=deepseek-r1:14b
# Dify Configuration
- DIFY_API_URL=http://datacenter.dldzxx.cn:8089/v1
- DIFY_API_KEY=app-lkK33EQOVXXrjD9x3SKbItr7
depends_on: depends_on:
- mongo - mongo
networks: networks:
......
...@@ -4,47 +4,56 @@ import com.llmfilter.edu.dto.*; ...@@ -4,47 +4,56 @@ import com.llmfilter.edu.dto.*;
import com.llmfilter.edu.security.UserContext; import com.llmfilter.edu.security.UserContext;
import com.llmfilter.edu.security.UserContextHolder; import com.llmfilter.edu.security.UserContextHolder;
import com.llmfilter.edu.service.DashboardService; import com.llmfilter.edu.service.DashboardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping("/api/v1/dashboard") @RequestMapping("/api/v1/dashboard")
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "Dashboard", description = "仪表盘数据接口")
public class DashboardController { public class DashboardController {
private final DashboardService dashboardService; private final DashboardService dashboardService;
@GetMapping("/student/today") @GetMapping("/student/today")
@Operation(summary = "学生今日概览", description = "获取当前学生用户的今日课程、出勤状态及待办事项等概览信息")
public StudentTodaySummary getStudentToday() { public StudentTodaySummary getStudentToday() {
UserContext user = UserContextHolder.getContext(); UserContext user = UserContextHolder.getContext();
return dashboardService.getStudentTodaySummary(user); return dashboardService.getStudentTodaySummary(user);
} }
@GetMapping("/student/week") @GetMapping("/student/week")
@Operation(summary = "学生周课表", description = "获取当前学生用户的周课表信息,支持指定周次(默认当前周)")
public StudentWeekSummary getStudentWeek(@RequestParam(required = false) Integer week) { public StudentWeekSummary getStudentWeek(@RequestParam(required = false) Integer week) {
UserContext user = UserContextHolder.getContext(); UserContext user = UserContextHolder.getContext();
return dashboardService.getStudentWeekSchedule(user, week); return dashboardService.getStudentWeekSchedule(user, week);
} }
@GetMapping("/teacher/week") @GetMapping("/teacher/week")
@Operation(summary = "教师周课表", description = "获取当前教师用户的周课表信息,支持指定周次(默认当前周)")
public TeacherWeekSummary getTeacherWeek(@RequestParam(required = false) Integer week) { public TeacherWeekSummary getTeacherWeek(@RequestParam(required = false) Integer week) {
UserContext user = UserContextHolder.getContext(); UserContext user = UserContextHolder.getContext();
return dashboardService.getTeacherWeekSchedule(user, week); return dashboardService.getTeacherWeekSchedule(user, week);
} }
@GetMapping("/homeroom/current") @GetMapping("/homeroom/current")
@Operation(summary = "班主任当前概览", description = "获取班主任所管理班级的当前出勤、请假及课堂状态信息")
public HomeroomCurrentSummary getHomeroomCurrent() { public HomeroomCurrentSummary getHomeroomCurrent() {
UserContext user = UserContextHolder.getContext(); UserContext user = UserContextHolder.getContext();
return dashboardService.getHomeroomCurrentSummary(user); return dashboardService.getHomeroomCurrentSummary(user);
} }
@GetMapping("/department/overview") @GetMapping("/department/overview")
@Operation(summary = "部门概览", description = "获取部门管理人员的部门整体出勤、教学活动及异常情况概览")
public DepartmentOverview getDepartmentOverview() { public DepartmentOverview getDepartmentOverview() {
UserContext user = UserContextHolder.getContext(); UserContext user = UserContextHolder.getContext();
return dashboardService.getDepartmentOverview(user); return dashboardService.getDepartmentOverview(user);
} }
@GetMapping("/campus/overview") @GetMapping("/campus/overview")
@Operation(summary = "校园概览", description = "获取校级管理人员的全校出勤率、课堂活跃度及安全预警概览")
public CampusOverview getCampusOverview() { public CampusOverview getCampusOverview() {
UserContext user = UserContextHolder.getContext(); UserContext user = UserContextHolder.getContext();
return dashboardService.getCampusOverview(user); return dashboardService.getCampusOverview(user);
......
...@@ -29,6 +29,12 @@ class Settings(BaseSettings): ...@@ -29,6 +29,12 @@ class Settings(BaseSettings):
OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", "llama2") OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", "llama2")
# Dify配置
DIFY_API_URL: str = os.getenv("DIFY_API_URL", "http://datacenter.dldzxx.cn:8089/v1")
DIFY_API_KEY: str = os.getenv("DIFY_API_KEY", "app-lkK33EQOVXXrjD9x3SKbItr7")
DIFY_RESPONSE_MODE: str = os.getenv("DIFY_RESPONSE_MODE", "streaming")
DIFY_MESSAGE_ENDPOINT: str = os.getenv("DIFY_MESSAGE_ENDPOINT", "chat-messages")
# 应用运行模式开关:仅运行教育版或企业版之一 # 应用运行模式开关:仅运行教育版或企业版之一
# 允许的值:"edu" / "biz";若未设置则默认使用 "edu" # 允许的值:"edu" / "biz";若未设置则默认使用 "edu"
# 注意:不再提供混合模式(mixed),如需混合请显式设置并在依赖中放行 # 注意:不再提供混合模式(mixed),如需混合请显式设置并在依赖中放行
......
...@@ -11,12 +11,11 @@ app = FastAPI( ...@@ -11,12 +11,11 @@ app = FastAPI(
version="1.0.0", version="1.0.0",
description=( description=(
"LLM 过滤系统后端接口\n\n" "LLM 过滤系统后端接口\n\n"
"本服务仅包含 LLM 对话、敏感词管理与仪表盘功能。\n" "本服务仅包含 LLM 对话与敏感词管理功能。\n"
"认证与用户管理请访问 Auth Service (8081)。\n" "认证与用户管理请访问 Auth Service (8081)。\n"
"教务数据管理请访问 Edu Service (8082)。" "教务数据管理请访问 Edu Service (8082)。"
), ),
openapi_tags=[ openapi_tags=[
{"name": "仪表盘", "description": "AI 使用统计看板"},
{"name": "管理员", "description": "管理员功能(敏感词、分类等)"}, {"name": "管理员", "description": "管理员功能(敏感词、分类等)"},
{"name": "对话", "description": "对话与敏感词审计接口"}, {"name": "对话", "description": "对话与敏感词审计接口"},
], ],
......
...@@ -4,6 +4,7 @@ from bson import ObjectId ...@@ -4,6 +4,7 @@ from bson import ObjectId
from app.db.mongodb import db from app.db.mongodb import db
from app.services.ollama import generate_response from app.services.ollama import generate_response
from app.utils.sensitive_word_filter import sensitive_word_filter from app.utils.sensitive_word_filter import sensitive_word_filter
from app.services.dify import dify_service
async def create_conversation(user_id: str) -> str: async def create_conversation(user_id: str) -> str:
"""创建新对话 """创建新对话
...@@ -111,6 +112,28 @@ async def add_message(conversation_id: str, user_id: str, content: str) -> Dict[ ...@@ -111,6 +112,28 @@ async def add_message(conversation_id: str, user_id: str, content: str) -> Dict[
sensitive_words = check_result["sensitive_words_found"] sensitive_words = check_result["sensitive_words_found"]
highest_severity = check_result["highest_severity"] highest_severity = check_result["highest_severity"]
# Dify 智能体二次过滤(仅当本地过滤通过时执行)
if not contains_sensitive:
dify_result = await dify_service.check_content_safety(content, user_id)
if not dify_result["safe"]:
contains_sensitive = True
dify_reason = dify_result.get("reason", "智能体识别为不安全内容")
dify_suggestion = dify_result.get("suggestion", "")
highest_severity = 4 # 设定为较高严重程度
# 构造一个虚拟的敏感词信息添加到列表
# 注意:sensitive_words 是一个列表,我们需要确保它是可变的
if sensitive_words is None:
sensitive_words = []
sensitive_words.append({
"word": "智能体拦截",
"category": "智能检测",
"subcategory": dify_reason,
"severity": 4,
"extra_info": dify_suggestion
})
# 详细敏感词信息(用于响应与审计) # 详细敏感词信息(用于响应与审计)
detailed_words: List[Dict[str, Any]] = [] detailed_words: List[Dict[str, Any]] = []
if contains_sensitive and sensitive_words: if contains_sensitive and sensitive_words:
......
import httpx
import json
import re
from typing import Dict, Any, Optional
from app.core.config import settings
class DifyService:
def __init__(self):
api_url = (settings.DIFY_API_URL or "").strip()
api_url = api_url.rstrip("/")
if api_url and not api_url.endswith("/v1"):
api_url = f"{api_url}/v1"
self.api_url = api_url
self.api_key = settings.DIFY_API_KEY
self.timeout = 60.0 # 增加超时时间,因为智能体处理可能较慢
self.response_mode = (settings.DIFY_RESPONSE_MODE or "streaming").strip().lower()
self.message_endpoint = (settings.DIFY_MESSAGE_ENDPOINT or "chat-messages").strip().strip("/")
def _parse_safety_answer(self, answer: str) -> Optional[Dict[str, Any]]:
if not answer:
return None
clean = answer.replace("```json", "").replace("```", "").strip()
try:
obj = json.loads(clean)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
pass
m = re.search(r"\{[\s\S]*\}", clean)
if not m:
return None
try:
obj = json.loads(m.group(0))
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
return None
async def _call_dify_blocking(self, url: str, payload: Dict[str, Any], headers: Dict[str, str]) -> str:
async with httpx.AsyncClient() as client:
response = await client.post(url, json=payload, headers=headers, timeout=self.timeout)
if response.status_code != 200:
print(f"Dify API Error: {response.text}")
return ""
data = response.json()
answer = data.get("answer", "")
return answer if isinstance(answer, str) else ""
async def _call_dify_streaming(self, url: str, payload: Dict[str, Any], headers: Dict[str, str]) -> str:
answer_chunks: list[str] = []
async with httpx.AsyncClient() as client:
async with client.stream("POST", url, json=payload, headers=headers, timeout=self.timeout) as response:
if response.status_code != 200:
try:
body = await response.aread()
print(f"Dify API Error: {body.decode('utf-8', errors='ignore')}")
except Exception:
print("Dify API Error: <failed to read response body>")
return ""
async for line in response.aiter_lines():
if not line:
continue
if not line.startswith("data:"):
continue
data_str = line[len("data:") :].strip()
if not data_str or data_str == "[DONE]":
continue
try:
event = json.loads(data_str)
except json.JSONDecodeError:
continue
if not isinstance(event, dict):
continue
ev_type = event.get("event")
if ev_type in {"message_end", "agent_message_end", "tts_message_end"}:
break
part = event.get("answer")
if isinstance(part, str) and part:
answer_chunks.append(part)
return "".join(answer_chunks).strip()
async def check_content_safety(self, content: str, user_id: str) -> Dict[str, Any]:
"""
使用 Dify 智能体检查内容安全
Args:
content: 用户输入内容
user_id: 用户ID
Returns:
Dict: {
"safe": bool, # 是否安全
"reason": str, # 如果不安全的原因
"suggestion": str # 修改建议(可选)
}
"""
# 如果未配置 API Key,默认跳过检查(返回安全)
if not self.api_key:
return {"safe": True, "reason": "", "suggestion": ""}
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# 构造 Dify 聊天消息请求
# 假设 Dify 应用已配置为返回特定 JSON 结构的文本
payload = {
"inputs": {},
"query": content,
"response_mode": self.response_mode,
"conversation_id": "", # 每次检查作为独立对话,或者可以关联上下文
"user": user_id
}
try:
if not self.api_url or not self.message_endpoint:
return {"safe": True, "reason": "Dify config missing", "suggestion": ""}
url = f"{self.api_url}/{self.message_endpoint}"
if self.response_mode == "blocking":
answer = await self._call_dify_blocking(url, payload, headers)
else:
answer = await self._call_dify_streaming(url, payload, headers)
parsed = self._parse_safety_answer(answer)
if not parsed:
if answer:
print(f"Dify response is not valid JSON: {answer}")
return {"safe": True, "reason": "Invalid JSON response", "suggestion": ""}
return {
"safe": parsed.get("safe", True),
"reason": parsed.get("reason", ""),
"suggestion": parsed.get("suggestion", "")
}
except Exception as e:
print(f"Error calling Dify: {str(e)}")
return {"safe": True, "reason": f"Error: {str(e)}", "suggestion": ""}
dify_service = DifyService()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment