Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions backend/app/api/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from . import graph_bp
from ..config import Config
from ..utils.request_locale import get_request_locale
from ..services.ontology_generator import OntologyGenerator
from ..services.graph_builder import GraphBuilderService
from ..services.text_processor import TextProcessor
Expand Down Expand Up @@ -211,13 +212,15 @@ def generate_ontology():
ProjectManager.save_extracted_text(project.project_id, all_text)
logger.info(f"文本提取完成,共 {len(all_text)} 字符")

# 生成本体
logger.info("调用 LLM 生成本体定义...")
# 生成本体(传递用户语言,LLM 输出与该语言一致)
locale = get_request_locale()
logger.info(f"调用 LLM 生成本体定义... (locale={locale})")
generator = OntologyGenerator()
ontology = generator.generate(
document_texts=document_texts,
simulation_requirement=simulation_requirement,
additional_context=additional_context if additional_context else None
additional_context=additional_context if additional_context else None,
language=locale
)

# 保存本体到项目
Expand Down
33 changes: 21 additions & 12 deletions backend/app/api/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from . import report_bp
from ..config import Config
from ..utils.request_locale import get_request_locale
from ..utils.error_messages import get_error_message
from ..services.report_agent import ReportAgent, ReportManager, ReportStatus
from ..services.simulation_manager import SimulationManager
from ..models.project import ProjectManager
Expand Down Expand Up @@ -105,6 +107,9 @@ def generate_report():
"error": "缺少模拟需求描述"
}), 400

# 获取用户语言偏好(在启动线程前捕获,线程内 request 可能不可用)
report_language = get_request_locale()

# 提前生成 report_id,以便立即返回给前端
import uuid
report_id = f"report_{uuid.uuid4().hex[:12]}"
Expand All @@ -130,11 +135,12 @@ def run_generate():
message="初始化Report Agent..."
)

# 创建Report Agent
# 创建Report Agent(传入用户语言,报告和对话均使用该语言)
agent = ReportAgent(
graph_id=graph_id,
simulation_id=simulation_id,
simulation_requirement=simulation_requirement
simulation_requirement=simulation_requirement,
report_language=report_language
)

# 进度回调
Expand Down Expand Up @@ -498,16 +504,17 @@ def chat_with_report_agent():
message = data.get('message')
chat_history = data.get('chat_history', [])

report_lang = get_request_locale()
if not simulation_id:
return jsonify({
"success": False,
"error": "请提供 simulation_id"
"error": get_error_message('missing_simulation_id', report_lang)
}), 400

if not message:
return jsonify({
"success": False,
"error": "请提供 message"
"error": get_error_message('missing_message', report_lang)
}), 400

# 获取模拟和项目信息
Expand All @@ -517,30 +524,32 @@ def chat_with_report_agent():
if not state:
return jsonify({
"success": False,
"error": f"模拟不存在: {simulation_id}"
"error": f"{get_error_message('simulation_not_found', report_lang)}: {simulation_id}"
}), 404

project = ProjectManager.get_project(state.project_id)
if not project:
return jsonify({
"success": False,
"error": f"项目不存在: {state.project_id}"
"error": f"{get_error_message('project_not_found', report_lang)}: {state.project_id}"
}), 404

graph_id = state.graph_id or project.graph_id
if not graph_id:
return jsonify({
"success": False,
"error": "缺少图谱ID"
"error": get_error_message('missing_graph_id', report_lang)
}), 400

simulation_requirement = project.simulation_requirement or ""
report_language = report_lang

# 创建Agent并进行对话
# 创建Agent并进行对话(使用用户选择的语言)
agent = ReportAgent(
graph_id=graph_id,
simulation_id=simulation_id,
simulation_requirement=simulation_requirement
simulation_requirement=simulation_requirement,
report_language=report_language
)

result = agent.chat(message=message, chat_history=chat_history)
Expand Down
15 changes: 13 additions & 2 deletions backend/app/services/ontology_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ def generate(
self,
document_texts: List[str],
simulation_requirement: str,
additional_context: Optional[str] = None
additional_context: Optional[str] = None,
language: str = 'zh'
) -> Dict[str, Any]:
"""
生成本体定义
Expand All @@ -177,10 +178,20 @@ def generate(
document_texts: 文档文本列表
simulation_requirement: 模拟需求描述
additional_context: 额外上下文
language: 输出语言 ('zh' 或 'en'),用于 analysis_summary 等字段

Returns:
本体定义(entity_types, edge_types等)
"""
self._language = language
# 语言指令:让 LLM 的 analysis_summary 等自然语言输出与用户语言一致
lang_instruction = (
"\n\n**语言要求**:analysis_summary 必须使用中文撰写。"
if language == 'zh' else
"\n\n**Language requirement**: You MUST write analysis_summary in English."
)
system_prompt = ONTOLOGY_SYSTEM_PROMPT + lang_instruction

# 构建用户消息
user_message = self._build_user_message(
document_texts,
Expand All @@ -189,7 +200,7 @@ def generate(
)

messages = [
{"role": "system", "content": ONTOLOGY_SYSTEM_PROMPT},
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
]

Expand Down
27 changes: 22 additions & 5 deletions backend/app/services/report_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,10 @@ def to_dict(self) -> Dict[str, Any]:

注意:sections数组最少2个,最多5个元素!"""

# 语言指令(根据 report_language 动态追加到各 prompt)
LANGUAGE_INSTRUCTION_ZH = "\n\n【语言要求】报告标题、摘要、章节标题和内容描述必须全部使用中文撰写。"
LANGUAGE_INSTRUCTION_EN = "\n\n【Language requirement】You MUST write the report title, summary, section titles and all content in English only. Think and respond in English."

PLAN_USER_PROMPT_TEMPLATE = """\
【预测场景设定】
我们向模拟世界注入的变量(模拟需求):{simulation_requirement}
Expand Down Expand Up @@ -653,8 +657,7 @@ def to_dict(self) -> Dict[str, Any]:

3. 【语言一致性 - 引用内容必须翻译为报告语言】
- 工具返回的内容可能包含英文或中英文混杂的表述
- 如果模拟需求和材料原文是中文的,报告必须全部使用中文撰写
- 当你引用工具返回的英文或中英混杂内容时,必须将其翻译为流畅的中文后再写入报告
- {language_consistency_rule}
- 翻译时保持原意不变,确保表述自然通顺
- 这一规则同时适用于正文和引用块(> 格式)中的内容

Expand Down Expand Up @@ -851,7 +854,9 @@ def to_dict(self) -> Dict[str, Any]:
【回答风格】
- 简洁直接,不要长篇大论
- 使用 > 格式引用关键内容
- 优先给出结论,再解释原因"""
- 优先给出结论,再解释原因

【语言】{chat_language_instruction}"""

CHAT_OBSERVATION_SUFFIX = "\n\n请简洁回答问题。"

Expand Down Expand Up @@ -886,7 +891,8 @@ def __init__(
simulation_id: str,
simulation_requirement: str,
llm_client: Optional[LLMClient] = None,
zep_tools: Optional[ZepToolsService] = None
zep_tools: Optional[ZepToolsService] = None,
report_language: str = 'zh'
):
"""
初始化Report Agent
Expand All @@ -897,10 +903,12 @@ def __init__(
simulation_requirement: 模拟需求描述
llm_client: LLM客户端(可选)
zep_tools: Zep工具服务(可选)
report_language: 报告输出语言 'zh' 或 'en',LLM 将用该语言思考和生成
"""
self.graph_id = graph_id
self.simulation_id = simulation_id
self.simulation_requirement = simulation_requirement
self.report_language = report_language if report_language in ('zh', 'en') else 'zh'

self.llm = llm_client or LLMClient()
self.zep_tools = zep_tools or ZepToolsService()
Expand Down Expand Up @@ -1162,7 +1170,8 @@ def plan_outline(
if progress_callback:
progress_callback("planning", 30, "正在生成报告大纲...")

system_prompt = PLAN_SYSTEM_PROMPT
lang_inst = LANGUAGE_INSTRUCTION_ZH if self.report_language == 'zh' else LANGUAGE_INSTRUCTION_EN
system_prompt = PLAN_SYSTEM_PROMPT + lang_inst
user_prompt = PLAN_USER_PROMPT_TEMPLATE.format(
simulation_requirement=self.simulation_requirement,
total_nodes=context.get('graph_statistics', {}).get('total_nodes', 0),
Expand Down Expand Up @@ -1251,9 +1260,15 @@ def _generate_section_react(
if self.report_logger:
self.report_logger.log_section_start(section.title, section_index)

lang_rule = (
"报告必须全部使用中文撰写。当你引用工具返回的英文或中英混杂内容时,必须将其翻译为流畅的中文后再写入报告。"
if self.report_language == 'zh' else
"The report MUST be written entirely in English. When you quote content from tools that is in Chinese or mixed language, translate it into fluent English before including it in the report."
)
system_prompt = SECTION_SYSTEM_PROMPT_TEMPLATE.format(
report_title=outline.title,
report_summary=outline.summary,
language_consistency_rule=lang_rule,
simulation_requirement=self.simulation_requirement,
section_title=section.title,
tools_description=self._get_tools_description(),
Expand Down Expand Up @@ -1800,10 +1815,12 @@ def chat(
except Exception as e:
logger.warning(f"获取报告内容失败: {e}")

chat_lang = "回复必须使用中文。" if self.report_language == 'zh' else "You MUST respond in English only."
system_prompt = CHAT_SYSTEM_PROMPT_TEMPLATE.format(
simulation_requirement=self.simulation_requirement,
report_content=report_content if report_content else "(暂无报告)",
tools_description=self._get_tools_description(),
chat_language_instruction=chat_lang,
)

# 构建消息
Expand Down
26 changes: 26 additions & 0 deletions backend/app/utils/error_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Error messages in zh/en for API responses.
"""

MESSAGES = {
'zh': {
'missing_simulation_id': '请提供 simulation_id',
'missing_message': '请提供 message',
'simulation_not_found': '模拟不存在',
'project_not_found': '项目不存在',
'missing_graph_id': '缺少图谱ID',
},
'en': {
'missing_simulation_id': 'Please provide simulation_id',
'missing_message': 'Please provide message',
'simulation_not_found': 'Simulation not found',
'project_not_found': 'Project not found',
'missing_graph_id': 'Missing graph ID',
},
}


def get_error_message(key: str, locale: str = 'zh') -> str:
"""Return localized error message. Fallback to zh if key missing for locale."""
lang = 'en' if locale == 'en' else 'zh'
return MESSAGES.get(lang, MESSAGES['zh']).get(key, MESSAGES['zh'].get(key, str(key)))
28 changes: 28 additions & 0 deletions backend/app/utils/request_locale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
从请求中获取用户选择的语言(用于 LLM 输出语言)
支持 X-Locale 请求头或 JSON body 中的 locale 参数
"""
from flask import request


def get_request_locale(default: str = 'zh') -> str:
"""
从当前请求中获取语言偏好。

- X-Locale: en 或 zh
- 或 request.get_json().get('locale')
- 或 request.form.get('locale')

Returns:
'en' 或 'zh'
"""
locale = request.headers.get('X-Locale', '').strip().lower()
if not locale and request.is_json:
data = request.get_json(silent=True) or {}
locale = (data.get('locale') or data.get('language') or '').strip().lower()
if not locale:
locale = (request.form.get('locale') or request.form.get('language') or '').strip().lower()

if locale in ('en', 'zh'):
return locale
return default
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"axios": "^1.13.2",
"d3": "^7.9.0",
"vue": "^3.5.24",
"vue-i18n": "^9.14.5",
"vue-router": "^4.6.3"
},
"devDependencies": {
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
</template>

<script setup>
// 使用 Vue Router 来管理页面
import { onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { getStoredLocale } from './i18n'

// Sync html lang attribute on mount
onMounted(() => {
const locale = getStoredLocale()
document.documentElement.lang = locale === 'zh' ? 'zh-CN' : 'en'
})
</script>

<style>
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/api/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from 'axios'
import { getApiLocale } from '../i18n'

// 创建axios实例
const service = axios.create({
Expand All @@ -9,9 +10,13 @@ const service = axios.create({
}
})

// 请求拦截器
// 请求拦截器 - 添加用户选择的语言,供后端 LLM 使用
service.interceptors.request.use(
config => {
const locale = getApiLocale()
if (locale) {
config.headers['X-Locale'] = locale
}
return config
},
error => {
Expand Down
Loading