import logging
import os
from enum import Enum
from typing import Any, TypedDict, Literal, Annotated, List
from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.config import get_stream_writer
from langgraph.constants import END
from langgraph.graph import StateGraph, add_messages
from langgraph.types import Command, interrupt
from langmem.short_term import SummarizationNode
from deepinsight.core.utils.tool_utils import create_retrieval_tool
from deepinsight.core.utils.progress_utils import progress_stage
from deepinsight.core.utils.research_utils import parse_research_config
from deepinsight.core.types.research import FinalResult
from deepinsight.core.types.graph_config import RetrievalType
from deepinsight.core.agent.conf_gen.supervisor import graph as conference_research_graph
from deepinsight.core.agent.conf_gen.cross_topic_supervisor import (
graph as cross_topic_graph,
construct_sub_config as construct_cross_topic_sub_config,
)
from deepinsight.core.agent.conf_chat.statistics import graph as statistics_graph
from deepinsight.core.tools.tavily_search import tavily_search
from deepinsight.core.tools.wordcloud_tool import generate_wordcloud
from deepinsight.service.schemas.research import SceneType
from deepinsight.utils.tavily_manager import tavily_key_manager, TavilyNoEnvError, TavilyNoAvailableKeyError
from integrations.mcps.generate_chart import generate_area_chart, generate_bar_chart, generate_column_chart, \
generate_pie_chart, generate_scatter_chart, generate_line_chart, generate_radar_chart
class GraphNodeType(str, Enum):
SUMMARIZER = "summarizer"
SUPERVISOR = "supervisor"
ANSWER_COMPOSER = "answer_composer"
CLARIFY_NODE = "question_clarify"
PAPER_TEAM = "paper_team"
RETRIVAL_TEAM = "retrival_team"
DEEP_RESEARCH_TEAM = "deep_research_team"
CHART_NODE = "chart_node"
CROSS_TOPIC_TEAM = "cross_topic_team"
class SupervisorState(TypedDict):
messages: Annotated[List[BaseMessage], add_messages]
context: dict[str, Any]
async def summarization_node(state: SupervisorState, config):
rc = parse_research_config(config)
summarizer = SummarizationNode(
model=rc.get_model(),
max_tokens=32768,
max_tokens_before_summary=2048,
max_summary_tokens=16384,
output_messages_key="messages"
)
result = await summarizer.ainvoke(state)
return result
@progress_stage("生成回复")
async def answer_composer_node(state: SupervisorState, config):
rc = parse_research_config(config)
prompt_template = rc.prompt_manager.get_prompt(
name="answer_composer_prompt",
group=rc.prompt_group,
)
system_prompt = prompt_template.format(
messages=state["messages"]
)
response = await rc.get_model().ainvoke([
{"role": "system", "content": system_prompt}
])
writer = get_stream_writer()
writer(FinalResult(
final_report=response.content
))
def make_supervisor_node():
def parse_response(response_content: str):
import json
"""解析模型返回的内容,提取next_step"""
try:
logging.debug(response_content)
return json.loads(response_content)
except Exception:
start = response_content.find('{')
end = response_content.rfind('}') + 1
if start != -1 and end != -1:
json_str = response_content[start:end]
return json.loads(json_str)
return None
async def supervisor_node(state: SupervisorState, config) -> Command[Literal[
GraphNodeType.CLARIFY_NODE,
GraphNodeType.PAPER_TEAM,
GraphNodeType.CHART_NODE,
GraphNodeType.RETRIVAL_TEAM,
GraphNodeType.DEEP_RESEARCH_TEAM,
GraphNodeType.CROSS_TOPIC_TEAM,
GraphNodeType.ANSWER_COMPOSER,
]]:
rc = parse_research_config(config)
kb_count = 0
conference_hint = ""
if rc.retrieval_config:
for retrieval_type, retrieval_config in rc.retrieval_config.items():
if hasattr(retrieval_config, 'args') and hasattr(retrieval_config.args, 'kb_ids'):
kb_ids = retrieval_config.args.kb_ids or []
kb_count = len(kb_ids)
if kb_count > 1:
conference_hint = (
f"\n\n【重要提示】当前请求涉及 {kb_count} 个会议的知识库。"
f"如果用户问题是关于技术主题的分析、对比或研究,"
f"应该考虑使用 cross_topic_team 进行跨会议主题分析。"
)
logging.info(f"检测到 {kb_count} 个知识库,提示 supervisor 考虑跨会议场景")
break
members = [
GraphNodeType.CLARIFY_NODE.value,
GraphNodeType.PAPER_TEAM.value,
GraphNodeType.CHART_NODE.value,
GraphNodeType.RETRIVAL_TEAM.value,
GraphNodeType.DEEP_RESEARCH_TEAM.value,
GraphNodeType.CROSS_TOPIC_TEAM.value,
]
members_description = {
GraphNodeType.CLARIFY_NODE: rc.prompt_manager.get_prompt(
name="clarify_node_prompt",
group=rc.prompt_group,
).format(),
GraphNodeType.PAPER_TEAM: rc.prompt_manager.get_prompt(
name="paper_team_prompt",
group=rc.prompt_group,
).format(),
GraphNodeType.RETRIVAL_TEAM: rc.prompt_manager.get_prompt(
name="retrieval_team_prompt",
group=rc.prompt_group,
).format(),
GraphNodeType.CHART_NODE: rc.prompt_manager.get_prompt(
name="report_team_prompt",
group=rc.prompt_group,
).format(),
GraphNodeType.DEEP_RESEARCH_TEAM: rc.prompt_manager.get_prompt(
name="deep_research_team_prompt",
group=rc.prompt_group,
).format(),
GraphNodeType.CROSS_TOPIC_TEAM: rc.prompt_manager.get_prompt(
name="cross_topic_team_prompt",
group=rc.prompt_group,
).format(),
}
members_str = "\n".join([f"- {m}" for m in members])
member_list_string = ', '.join([f'"{node}"' for node in members])
members_desc_str = "\n".join(
[f"- **{k.value}**: {v.strip()}" for k, v in members_description.items()]
)
prompt_template = rc.prompt_manager.get_prompt(
name="supervisor_prompt",
group=rc.prompt_group,
)
conf_analysis_supervisor_prompt = prompt_template.format(
members=members_str,
members_description=members_desc_str,
member_list=member_list_string
)
if conference_hint:
conf_analysis_supervisor_prompt += conference_hint
messages = [
{"role": "system", "content": conf_analysis_supervisor_prompt},
] + state["messages"]
llm = rc.get_model()
response = await llm.ainvoke(messages)
llm_response = response.content
result = parse_response(llm_response)
if not result or result["next"] == END or result["next"] == GraphNodeType.CLARIFY_NODE.value:
return Command(
goto=GraphNodeType.ANSWER_COMPOSER.value,
update={"messages": {"role": "ai", "content": llm_response}}
)
return Command(
goto=result["next"]
)
return supervisor_node
async def question_clarify_node(state: SupervisorState) -> Command[Literal[GraphNodeType.SUMMARIZER]]:
user_reply = interrupt(state["messages"][-1].content)
return Command(goto=GraphNodeType.SUMMARIZER.value, update={
"messages": HumanMessage(
content=user_reply
)
})
@progress_stage("论文统计分析")
async def paper_team_node(state: SupervisorState) -> Command[Literal[GraphNodeType.SUMMARIZER]]:
result = await statistics_graph.ainvoke(
{"messages": [("user", state["messages"][-1].content)]},
{"recursion_limit": 100},
)
return Command(goto=GraphNodeType.SUMMARIZER.value, update={
"messages": HumanMessage(
content=result["messages"][-1].content, name="paper_team"
)
})
@progress_stage("论文检索")
async def retrival_team_node(state: SupervisorState, config: RunnableConfig) -> Command[Literal[END]]:
rc = parse_research_config(config)
tools = [tavily_search]
if rc.retrieval_config:
for retrieval_type in rc.retrieval_config.keys():
try:
retrieval_tool = create_retrieval_tool(retrieval_type, config)
tools.append(retrieval_tool)
except Exception as e:
logging.warning(f"Failed to create retrieval tool for {retrieval_type}: {e}")
system_prompt = """
你是一名专精于学术论文检索与数据分析的智能研究助理。
你的任务是根据用户请求,**高效查询学术论文、会议论文及相关作者/机构信息**。
你应当优先利用 RAG 检索(ragflow)进行信息查找;
若 RAG 不可用或返回结果为空,则自动使用 Tavily 搜索工具进行查询。
---
### 🎯 工作目标
1. **优先使用 RAG 检索知识库(ragflow)获取论文、作者、会议信息。**
2. **当 RAG 检索无结果或无法使用时,自动切换至 Tavily 搜索。**
3. **每次查询完成后,主动反思结果是否满足用户问题。**
- 如果结果不完整或不相关,请优化查询语句(query)并重试。
4. **最多尝试 5 次查询。**
- 若超过 5 次仍未找到有效结果,则返回检索失败的提示(例如:“未能检索到相关论文或信息”)。
---
### 🧰 可用工具
- **ragflow.knowledge_retrieve**:RAG 检索学术知识库内容。
- **tavily_search**:从互联网检索学术论文、会议及作者信息。
---
### 🧠 检索与反思流程
每次检索请执行以下逻辑:
1. **执行查询**
* 优先使用 ragflow 进行知识检索;
* 若 RAG 无法使用或结果为空,则改用 tavily_search。
2. **结果评估**
* 检查结果是否满足用户问题;
* 如果不满足,重写查询语句并重试。
3. **重试机制**
* 最多执行 5 次;
* 超过 5 次仍无结果则返回失败信息。
---
### 📘 输出要求
* 所有内容必须来源于检索结果,不得编造、估算或推断。
* 回答要简洁、准确,并与用户问题直接相关。
* 若涉及数据库查询,请仅使用 PythonREPLTool 和 SQLAlchemy。
* 不进行统计、趋势分析或额外评论,除非用户明确要求。
---
### 📄 输出格式规范(新增)
每次输出结果时,请严格按照以下格式组织内容:
#### ✅ 标准输出格式
```
<在此展示检索得到的论文摘要、会议介绍或作者信息原文,不做改写>
【来源】
* 来源类型:RAG 检索 / 网络检索(Tavily)
* 来源名称:<数据库名或网站名,如 “IEEE Xplore”, “ACM Digital Library”, “Google Scholar”, “arXiv”, “SpringerLink” 等>
* 检索时间:<自动填入检索执行的时间,如 2025-11-12 14:35>
* 原始链接(若有):<论文或数据源的具体链接>
```
#### ✅ 多条结果输出格式
若返回多篇论文或多个来源,请以编号形式列出:
```
<论文摘要或核心内容>
【来源】
* 来源类型:RAG 检索
* 来源名称:ACM Digital Library
* 检索时间:2025-11-12 14:35
* 原始链接:[https://dl.acm.org/](https://dl.acm.org/)...
* 来源类型:网络检索(Tavily)
* 来源名称:Google Scholar
* 检索时间:2025-11-12 14:36
* 原始链接:[https://scholar.google.com/](https://scholar.google.com/)...
```
---
### 🧩 附加说明
* 若检索失败,请输出:
```
❌ 未能检索到相关论文或信息,请尝试更换关键词或调整查询范围。
```
* 若部分结果存在信息缺失,请明确标注“[信息缺失]”。
"""
agent = create_agent(
model=rc.get_model(),
tools=tools,
middleware=[TodoListMiddleware()],
system_prompt=system_prompt
)
result = await agent.ainvoke(state, config=config)
return Command(
goto=GraphNodeType.SUMMARIZER.value,
update={
"messages": [
HumanMessage(content=result["messages"][-1].content, name="retrival_team")
]
}
)
@progress_stage("图表生成")
async def chart_node(state: SupervisorState, config: RunnableConfig) -> Command[Literal[GraphNodeType.SUMMARIZER]]:
rc = parse_research_config(config)
llm = rc.get_model()
chart_tools = [generate_area_chart, generate_bar_chart, generate_column_chart, generate_pie_chart,
generate_scatter_chart, generate_line_chart, generate_radar_chart, generate_wordcloud]
system_prompt = rc.prompt_manager.get_prompt(
name="report_chart_agent_sys_prompt",
group=rc.prompt_group,
).format()
agent = create_agent(
model=llm,
tools=chart_tools,
system_prompt=system_prompt
)
result = await agent.ainvoke(
{
"user_input": state["messages"][-1].content,
"messages": [
{"role": "human", "content": state["messages"][-1].content}
],
"charts": [],
"report": "",
})
return Command(goto=GraphNodeType.SUMMARIZER.value, update={
"messages": HumanMessage(
content=result["messages"][-1].content, name="report_team"
)
})
@progress_stage("顶会深度研究")
async def deep_research_team_node(state: SupervisorState, config: RunnableConfig) -> Command[Literal[END]]:
try:
tavily_key_manager().get_client()
except (TavilyNoEnvError, TavilyNoAvailableKeyError):
logging.error("no tavily key can be used, please set first.")
writer = get_stream_writer()
writer(FinalResult(
final_report="no tavily key can be used, please set first."
))
return Command(goto=END)
parent_configurable = config.get("configurable", {})
deep_research_config = {
**parent_configurable,
"prompt_group": "conf_gen_supervisor",
"allow_user_clarification": False,
"allow_edit_research_brief": False,
"allow_edit_report_outline": False,
"allow_publish_result": False,
}
result = await conference_research_graph.with_config(configurable=deep_research_config).ainvoke(
{"messages": [("user", state["messages"][-1].content)]}
)
writer = get_stream_writer()
writer({"result": result["messages"][-1].content})
return Command(goto=END, update={
"messages": HumanMessage(
content=result["messages"][-1].content, name="deep_research_team"
)
})
@progress_stage("跨会议主题分析")
async def cross_topic_team_node(state: SupervisorState, config: RunnableConfig) -> Command[Literal[END]]:
"""
跨会议主题分析团队节点
- 仍然在 CONFERENCE_QA 场景下工作,由 supervisor_prompt 决定是否派单到本节点
- kb_ids 从 config.retrieval_config 中获取,与 deep_research_team_node 保持一致
- 使用 conf_gen_cross_topic 的 prompt_group 和 cross_topic_supervisor 图完成跨会议分析,
生成统计信息、论文分析和总结等 MD 文件。
"""
rc = parse_research_config(config)
question = state["messages"][-1].content
writer = get_stream_writer()
sub_config = await construct_cross_topic_sub_config(config, prompt_group="conf_gen_cross_topic")
initial_state = {
"messages": [("user", question)],
"question": question,
}
try:
result = await cross_topic_graph.with_config(
configurable=sub_config
).ainvoke(initial_state)
if result.get("messages") and len(result["messages"]) > 0:
full_report = result["messages"][-1].content
else:
output_path = result.get("output_path", "")
full_report = None
if output_path:
try:
from deepinsight.core.types.conference_constants import (
ConferenceFileNames,
ConferenceFolderNames,
)
statistics_path = ConferenceFileNames.CROSS_TOPIC_STATISTICS_MD
summary_path = ConferenceFileNames.CROSS_TOPIC_SUMMARY_MD
papers_dir = ConferenceFolderNames.CROSS_TOPIC_PAPERS
papers_list_path = "papers_list.md"
parts = []
if rc.file_system.is_file(statistics_path):
parts.append(f"# 统计信息\n\n{rc.file_system.read(statistics_path)}")
if rc.file_system.is_dir(papers_dir):
paper_files = sorted([(name, content) for name, content in rc.file_system.read_all(papers_dir)
if name.endswith(".md")])
if paper_files:
parts.append("\n\n# 论文分析\n\n")
for _, content in paper_files:
parts.append(content)
parts.append("\n\n---\n\n")
if rc.file_system.is_file(summary_path):
parts.append(f"# 总结\n\n{rc.file_system.read(summary_path)}")
if rc.file_system.is_file(papers_list_path):
parts.append(f"\n\n# 论文列表\n\n{rc.file_system.read(papers_list_path)}")
if parts:
from datetime import datetime
now = datetime.now()
time_str = now.strftime("%Y年%m月%d日 %H时%M分%S秒")
header = (
f"# 跨会议主题分析报告\n\n"
f"**研究主题**:{question}\n\n"
f"**生成时间**:{time_str}\n\n"
f"---\n\n"
)
full_report = header + "\n\n".join(parts)
except Exception as e:
logging.warning(f"读取完整报告失败: {e}")
if not full_report:
output_path = result.get("output_path", "")
full_report = (
"跨会议主题分析已完成。\n\n"
f"- 输出目录:{output_path}\n"
"- 包含内容:统计信息(cross_topic_statistics.md)、"
"论文分析(cross_topic_papers/*.md)、总结(cross_topic_summary.md)以及论文列表(papers_list.md)。"
)
writer(FinalResult(final_report=full_report))
return Command(
goto=END,
update={
"messages": HumanMessage(
content=full_report,
name="cross_topic_team",
)
},
)
except Exception as e:
logging.exception("跨会议主题分析失败")
error_msg = f"跨会议主题分析失败:{e}"
writer(FinalResult(final_report=error_msg))
return Command(
goto=END,
update={
"messages": HumanMessage(
content=error_msg,
name="cross_topic_team",
)
},
)
builder = StateGraph(SupervisorState)
builder.add_node(GraphNodeType.SUMMARIZER.value, summarization_node)
builder.add_node(GraphNodeType.SUPERVISOR.value, make_supervisor_node())
builder.add_node(GraphNodeType.CLARIFY_NODE.value, question_clarify_node)
builder.add_node(GraphNodeType.PAPER_TEAM.value, paper_team_node)
builder.add_node(GraphNodeType.CHART_NODE.value, chart_node)
builder.add_node(GraphNodeType.RETRIVAL_TEAM.value, retrival_team_node)
builder.add_node(GraphNodeType.DEEP_RESEARCH_TEAM.value, deep_research_team_node)
builder.add_node(GraphNodeType.CROSS_TOPIC_TEAM.value, cross_topic_team_node)
builder.add_node(GraphNodeType.ANSWER_COMPOSER.value, answer_composer_node)
builder.set_entry_point(GraphNodeType.SUMMARIZER.value)
builder.add_edge(GraphNodeType.SUMMARIZER.value, GraphNodeType.SUPERVISOR.value)
builder.add_edge(GraphNodeType.ANSWER_COMPOSER.value, END)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)