MicroFish/backend/app/services/report_agent.py

818 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Report Agent 服务
使用 ReACT 模式的模拟报告生成 Agent。
Prompts → report_prompts.py | Data classes → report_data.py | Analytics → simulation_analytics.py
"""
import os
import json
import time
import re
from typing import Dict, Any, List, Optional, Callable
from ..config import Config
from ..utils.llm_client import LLMClient
from ..utils.logger import get_logger
from ..utils.locale import get_language_instruction, t
from .zep_tools import ZepToolsService
from .simulation_analytics import SimulationAnalyticsService
from .report_prompts import (
TOOL_DESC_SIMULATION_ANALYTICS,
TOOL_DESC_INSIGHT_FORGE,
TOOL_DESC_PANORAMA_SEARCH,
TOOL_DESC_QUICK_SEARCH,
TOOL_DESC_INTERVIEW_AGENTS,
PLAN_SYSTEM_PROMPT,
PLAN_USER_PROMPT_TEMPLATE,
SECTION_SYSTEM_PROMPT_TEMPLATE,
SECTION_USER_PROMPT_TEMPLATE,
REACT_OBSERVATION_TEMPLATE,
REACT_INSUFFICIENT_TOOLS_MSG,
REACT_INSUFFICIENT_TOOLS_MSG_ALT,
REACT_TOOL_LIMIT_MSG,
REACT_UNUSED_TOOLS_HINT,
REACT_FORCE_FINAL_MSG,
CHAT_SYSTEM_PROMPT_TEMPLATE,
CHAT_OBSERVATION_SUFFIX,
)
from .report_data import (
ReportStatus,
ReportSection,
ReportOutline,
Report,
ReportLogger,
ReportConsoleLogger,
ReportManager,
)
logger = get_logger('foresight.report_agent')
class ReportAgent:
"""
Report Agent — 模拟报告生成 Agent
ReACT 模式:规划 → 逐章节生成(工具调用 + LLM→ 组装
"""
MAX_TOOL_CALLS_PER_SECTION = 5
MAX_REFLECTION_ROUNDS = 3
MAX_TOOL_CALLS_PER_CHAT = 2
def __init__(
self,
graph_id: str,
simulation_id: str,
simulation_requirement: str,
llm_client: Optional[LLMClient] = None,
zep_tools: Optional[ZepToolsService] = None,
):
self.graph_id = graph_id
self.simulation_id = simulation_id
self.simulation_requirement = simulation_requirement
self.llm = llm_client or LLMClient(
api_key=os.environ.get("REPORT_LLM_API_KEY") or None,
base_url=os.environ.get("REPORT_LLM_BASE_URL") or None,
model=os.environ.get("REPORT_LLM_MODEL_NAME") or None,
)
self.zep_tools = zep_tools or ZepToolsService(llm_client=self.llm)
self.analytics = SimulationAnalyticsService()
self.tools = self._define_tools()
self.report_logger: Optional[ReportLogger] = None
self.console_logger: Optional[ReportConsoleLogger] = None
logger.info(t('report.agentInitDone', graphId=graph_id, simulationId=simulation_id))
# ── Tool Definitions ──
def _define_tools(self) -> Dict[str, Dict[str, Any]]:
return {
"simulation_analytics": {
"name": "simulation_analytics",
"description": TOOL_DESC_SIMULATION_ANALYTICS,
"parameters": {
"query_type": "查询类型: overview_stats, top_posts, agent_quotes, action_distribution, engagement_metrics, sentiment_breakdown",
"n": "返回数量可选默认10",
},
},
"insight_forge": {
"name": "insight_forge",
"description": TOOL_DESC_INSIGHT_FORGE,
"parameters": {
"query": "你想深入分析的问题或话题",
"report_context": "当前报告章节的上下文(可选)",
},
},
"panorama_search": {
"name": "panorama_search",
"description": TOOL_DESC_PANORAMA_SEARCH,
"parameters": {
"query": "搜索查询,用于相关性排序",
"include_expired": "是否包含过期/历史内容默认True",
},
},
"quick_search": {
"name": "quick_search",
"description": TOOL_DESC_QUICK_SEARCH,
"parameters": {
"query": "搜索查询字符串",
"limit": "返回结果数量可选默认10",
},
},
"interview_agents": {
"name": "interview_agents",
"description": TOOL_DESC_INTERVIEW_AGENTS,
"parameters": {
"interview_topic": "采访主题或需求描述",
"max_agents": "最多采访的Agent数量可选默认5",
},
},
}
VALID_TOOL_NAMES = {
"simulation_analytics", "insight_forge", "panorama_search",
"quick_search", "interview_agents",
}
# ── Tool Execution ──
def _execute_tool(self, tool_name: str, parameters: Dict[str, Any],
report_context: str = "") -> str:
logger.info(t('report.executingTool', toolName=tool_name, params=parameters))
try:
if tool_name == "simulation_analytics":
query_type = parameters.get("query_type", "overview_stats")
n = parameters.get("n", 10)
if isinstance(n, str):
n = int(n)
result = self.analytics.get_analytics(
simulation_id=self.simulation_id,
query_type=query_type,
n=n,
)
return json.dumps(result, ensure_ascii=False, indent=2)
elif tool_name == "insight_forge":
query = parameters.get("query", "")
ctx = parameters.get("report_context", "") or report_context
result = self.zep_tools.insight_forge(
graph_id=self.graph_id, query=query,
simulation_requirement=self.simulation_requirement,
report_context=ctx,
)
return result.to_text()
elif tool_name == "panorama_search":
query = parameters.get("query", "")
include_expired = parameters.get("include_expired", True)
if isinstance(include_expired, str):
include_expired = include_expired.lower() in ('true', '1', 'yes')
result = self.zep_tools.panorama_search(
graph_id=self.graph_id, query=query,
include_expired=include_expired,
)
return result.to_text()
elif tool_name == "quick_search":
query = parameters.get("query", "")
limit = parameters.get("limit", 10)
if isinstance(limit, str):
limit = int(limit)
result = self.zep_tools.quick_search(
graph_id=self.graph_id, query=query, limit=limit,
)
return result.to_text()
elif tool_name == "interview_agents":
topic = parameters.get("interview_topic", parameters.get("query", ""))
max_agents = parameters.get("max_agents", 5)
if isinstance(max_agents, str):
max_agents = int(max_agents)
result = self.zep_tools.interview_agents(
simulation_id=self.simulation_id,
interview_requirement=topic,
simulation_requirement=self.simulation_requirement,
max_agents=min(max_agents, 10),
)
return result.to_text()
# Backward compat aliases
elif tool_name == "search_graph":
return self._execute_tool("quick_search", parameters, report_context)
elif tool_name == "get_simulation_context":
query = parameters.get("query", self.simulation_requirement)
return self._execute_tool("insight_forge", {"query": query}, report_context)
elif tool_name == "get_graph_statistics":
result = self.zep_tools.get_graph_statistics(self.graph_id)
return json.dumps(result, ensure_ascii=False, indent=2)
elif tool_name == "get_entity_summary":
name = parameters.get("entity_name", "")
result = self.zep_tools.get_entity_summary(
graph_id=self.graph_id, entity_name=name,
)
return json.dumps(result, ensure_ascii=False, indent=2)
elif tool_name == "get_entities_by_type":
etype = parameters.get("entity_type", "")
nodes = self.zep_tools.get_entities_by_type(
graph_id=self.graph_id, entity_type=etype,
)
return json.dumps([n.to_dict() for n in nodes], ensure_ascii=False, indent=2)
else:
return f"未知工具: {tool_name}。请使用: simulation_analytics, insight_forge, panorama_search, quick_search, interview_agents"
except Exception as e:
logger.error(t('report.toolExecFailed', toolName=tool_name, error=str(e)))
return f"工具执行失败: {str(e)}"
# ── Tool Call Parsing ──
def _parse_tool_calls(self, response: str) -> List[Dict[str, Any]]:
tool_calls = []
# Format 1: XML-style
xml_pattern = r'edisnormal\s*(\{.*?\})\s*edisnormal'
for match in re.finditer(xml_pattern, response, re.DOTALL):
try:
tool_calls.append(json.loads(match.group(1)))
except json.JSONDecodeError:
pass
if tool_calls:
return tool_calls
# Format 2: Bare JSON
stripped = response.strip()
if stripped.startswith('{') and stripped.endswith('}'):
try:
data = json.loads(stripped)
if self._is_valid_tool_call(data):
tool_calls.append(data)
return tool_calls
except json.JSONDecodeError:
pass
# Format 3: Trailing JSON with tool name
json_pattern = r'(\{"(?:name|tool)"\s*:.*?\})\s*$'
match = re.search(json_pattern, stripped, re.DOTALL)
if match:
try:
data = json.loads(match.group(1))
if self._is_valid_tool_call(data):
tool_calls.append(data)
except json.JSONDecodeError:
pass
return tool_calls
def _is_valid_tool_call(self, data: dict) -> bool:
tool_name = data.get("name") or data.get("tool")
if tool_name and tool_name in self.VALID_TOOL_NAMES:
if "tool" in data:
data["name"] = data.pop("tool")
if "params" in data and "parameters" not in data:
data["parameters"] = data.pop("params")
return True
return False
def _get_tools_description(self) -> str:
parts = ["可用工具:"]
for name, tool in self.tools.items():
params_desc = ", ".join(f"{k}: {v}" for k, v in tool["parameters"].items())
parts.append(f"- {name}: {tool['description']}")
if params_desc:
parts.append(f" 参数: {params_desc}")
return "\n".join(parts)
# ── Outline Planning ──
def plan_outline(self, progress_callback: Optional[Callable] = None) -> ReportOutline:
logger.info(t('report.startPlanningOutline'))
if progress_callback:
progress_callback("planning", 0, t('progress.analyzingRequirements'))
context = self.zep_tools.get_simulation_context(
graph_id=self.graph_id,
simulation_requirement=self.simulation_requirement,
)
if progress_callback:
progress_callback("planning", 30, t('progress.generatingOutline'))
# Get simulation analytics for the plan prompt
analytics_overview = self.analytics.get_overview_stats(self.simulation_id)
sentiment = self.analytics.get_sentiment_breakdown(self.simulation_id)
system_prompt = f"{PLAN_SYSTEM_PROMPT}\n\n{get_language_instruction()}"
user_prompt = PLAN_USER_PROMPT_TEMPLATE.format(
simulation_requirement=self.simulation_requirement,
total_nodes=context.get('graph_statistics', {}).get('total_nodes', 0),
total_edges=context.get('graph_statistics', {}).get('total_edges', 0),
entity_types=list(context.get('graph_statistics', {}).get('entity_types', {}).keys()),
total_entities=context.get('total_entities', 0),
related_facts_json=json.dumps(context.get('related_facts', [])[:10], ensure_ascii=False, indent=2),
total_rounds=analytics_overview.get('total_rounds', 0),
total_agents=analytics_overview.get('total_agents', 0),
twitter_posts=analytics_overview.get('twitter_posts', 0),
reddit_posts=analytics_overview.get('reddit_posts', 0),
total_engagement=analytics_overview.get('total_engagement', 0),
positive_ratio=sentiment.get('positive_ratio', 0),
negative_ratio=sentiment.get('negative_ratio', 0),
)
try:
response = self.llm.chat_json(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=0.3,
)
if progress_callback:
progress_callback("planning", 80, t('progress.parsingOutline'))
sections = [
ReportSection(title=s.get("title", ""), content="")
for s in response.get("sections", [])
]
outline = ReportOutline(
title=response.get("title", "模拟分析报告"),
summary=response.get("summary", ""),
sections=sections,
)
if progress_callback:
progress_callback("planning", 100, t('progress.outlinePlanComplete'))
logger.info(t('report.outlinePlanDone', count=len(sections)))
return outline
except Exception as e:
logger.error(t('report.outlinePlanFailed', error=str(e)))
return ReportOutline(
title="未来预测报告",
summary="基于模拟预测的未来趋势与风险分析",
sections=[
ReportSection(title="预测场景与核心发现"),
ReportSection(title="人群行为预测分析"),
ReportSection(title="趋势展望与风险提示"),
],
)
# ── Section Generation (ReACT) ──
def _generate_section_react(
self,
section: ReportSection,
outline: ReportOutline,
previous_sections: List[str],
progress_callback: Optional[Callable] = None,
section_index: int = 0,
) -> str:
logger.info(t('report.reactGenerateSection', title=section.title))
if self.report_logger:
self.report_logger.log_section_start(section.title, section_index)
system_prompt = SECTION_SYSTEM_PROMPT_TEMPLATE.format(
report_title=outline.title,
report_summary=outline.summary,
simulation_requirement=self.simulation_requirement,
section_title=section.title,
tools_description=self._get_tools_description(),
)
system_prompt = f"{system_prompt}\n\n{get_language_instruction()}"
if previous_sections:
parts = [sec[:4000] + "..." if len(sec) > 4000 else sec for sec in previous_sections]
previous_content = "\n\n---\n\n".join(parts)
else:
previous_content = "(这是第一个章节)"
user_prompt = SECTION_USER_PROMPT_TEMPLATE.format(
previous_content=previous_content,
section_title=section.title,
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
tool_calls_count = 0
max_iterations = 6
min_tool_calls = 3
conflict_retries = 0
used_tools = set()
all_tools = {"simulation_analytics", "insight_forge", "panorama_search", "quick_search", "interview_agents"}
report_context = f"章节: {section.title}\n需求: {self.simulation_requirement}"
for iteration in range(max_iterations):
if progress_callback:
progress_callback(
"generating",
int((iteration / max_iterations) * 100),
t('progress.deepSearchAndWrite', current=tool_calls_count, max=self.MAX_TOOL_CALLS_PER_SECTION),
)
response = self.llm.chat(
messages=messages,
temperature=0.5,
max_tokens=8192,
)
if response is None:
logger.warning(t('report.sectionIterNone', title=section.title, iteration=iteration + 1))
if iteration < max_iterations - 1:
messages.append({"role": "assistant", "content": "(响应为空)"})
messages.append({"role": "user", "content": "请继续生成内容。"})
continue
break
tool_calls = self._parse_tool_calls(response)
has_tool_calls = bool(tool_calls)
has_final_answer = "Final Answer:" in response
# Conflict: both tool calls and Final Answer
if has_tool_calls and has_final_answer:
conflict_retries += 1
if conflict_retries <= 2:
messages.append({"role": "assistant", "content": response})
messages.append({
"role": "user",
"content": (
"【格式错误】你在一次回复中同时包含了工具调用和 Final Answer。\n"
"每次回复只能做一件事:调用工具 或 输出 Final Answer。请重新回复。"
),
})
continue
else:
first_end = response.find('edisnormal', response.find('edisnormal') + 10)
if first_end != -1:
response = response[:first_end + len('edisnormal')]
tool_calls = self._parse_tool_calls(response)
has_tool_calls = bool(tool_calls)
has_final_answer = False
conflict_retries = 0
if self.report_logger:
self.report_logger.log_llm_response(
section.title, section_index, response,
iteration + 1, has_tool_calls, has_final_answer,
)
# Case 1: Final Answer
if has_final_answer:
if tool_calls_count < min_tool_calls:
unused = all_tools - used_tools
hint = f"(推荐使用: {', '.join(unused)}" if unused else ""
messages.append({"role": "assistant", "content": response})
messages.append({
"role": "user",
"content": REACT_INSUFFICIENT_TOOLS_MSG.format(
tool_calls_count=tool_calls_count,
min_tool_calls=min_tool_calls,
unused_hint=hint,
),
})
continue
final_answer = response.split("Final Answer:")[-1].strip()
if self.report_logger:
self.report_logger.log_section_content(
section.title, section_index, final_answer, tool_calls_count,
)
return final_answer
# Case 2: Tool call
if has_tool_calls:
if tool_calls_count >= self.MAX_TOOL_CALLS_PER_SECTION:
messages.append({"role": "assistant", "content": response})
messages.append({
"role": "user",
"content": REACT_TOOL_LIMIT_MSG.format(
tool_calls_count=tool_calls_count,
max_tool_calls=self.MAX_TOOL_CALLS_PER_SECTION,
),
})
continue
call = tool_calls[0]
if len(tool_calls) > 1:
logger.info(t('report.multiToolOnlyFirst', total=len(tool_calls), toolName=call['name']))
if self.report_logger:
self.report_logger.log_tool_call(
section.title, section_index, call["name"],
call.get("parameters", {}), iteration + 1,
)
result = self._execute_tool(call["name"], call.get("parameters", {}), report_context)
if self.report_logger:
self.report_logger.log_tool_result(
section.title, section_index, call["name"], result, iteration + 1,
)
tool_calls_count += 1
used_tools.add(call['name'])
unused = all_tools - used_tools
unused_hint = ""
if unused and tool_calls_count < self.MAX_TOOL_CALLS_PER_SECTION:
unused_hint = REACT_UNUSED_TOOLS_HINT.format(unused_list="".join(unused))
analytics_hint = ""
if call["name"] == "simulation_analytics":
analytics_hint = "\n💡 simulation_analytics 返回了硬数据,请在 Final Answer 中引用具体数字!\n"
messages.append({"role": "assistant", "content": response})
messages.append({
"role": "user",
"content": REACT_OBSERVATION_TEMPLATE.format(
tool_name=call["name"],
result=result,
tool_calls_count=tool_calls_count,
max_tool_calls=self.MAX_TOOL_CALLS_PER_SECTION,
used_tools_str=", ".join(used_tools),
unused_hint=unused_hint,
analytics_hint=analytics_hint,
),
})
continue
# Case 3: Neither tool call nor Final Answer
messages.append({"role": "assistant", "content": response})
if tool_calls_count < min_tool_calls:
unused = all_tools - used_tools
hint = f"(推荐使用: {', '.join(unused)}" if unused else ""
messages.append({
"role": "user",
"content": REACT_INSUFFICIENT_TOOLS_MSG_ALT.format(
tool_calls_count=tool_calls_count,
min_tool_calls=min_tool_calls,
unused_hint=hint,
),
})
continue
# Enough tools called, treat as final answer
logger.info(t('report.sectionNoPrefix', title=section.title, count=tool_calls_count))
final_answer = response.strip()
if self.report_logger:
self.report_logger.log_section_content(
section.title, section_index, final_answer, tool_calls_count,
)
return final_answer
# Max iterations reached
logger.warning(t('report.sectionMaxIter', title=section.title))
messages.append({"role": "user", "content": REACT_FORCE_FINAL_MSG})
response = self.llm.chat(messages=messages, temperature=0.5, max_tokens=8192)
if response is None:
final_answer = t('report.sectionGenFailedContent')
elif "Final Answer:" in response:
final_answer = response.split("Final Answer:")[-1].strip()
else:
final_answer = response
if self.report_logger:
self.report_logger.log_section_content(
section.title, section_index, final_answer, tool_calls_count,
)
return final_answer
# ── Full Report Generation ──
def generate_report(
self,
progress_callback: Optional[Callable[[str, int, str], None]] = None,
report_id: Optional[str] = None,
) -> Report:
import uuid
if not report_id:
report_id = f"report_{uuid.uuid4().hex[:12]}"
start_time = time.time()
report = Report(
report_id=report_id,
simulation_id=self.simulation_id,
graph_id=self.graph_id,
simulation_requirement=self.simulation_requirement,
status=ReportStatus.PENDING,
created_at=time.strftime('%Y-%m-%dT%H:%M:%S'),
)
completed_section_titles = []
try:
ReportManager._ensure_report_folder(report_id)
self.report_logger = ReportLogger(report_id)
self.report_logger.log_start(
self.simulation_id, self.graph_id, self.simulation_requirement,
)
self.console_logger = ReportConsoleLogger(report_id)
ReportManager.update_progress(report_id, "pending", 0, t('progress.initReport'), completed_sections=[])
ReportManager.save_report(report)
# Phase 1: Plan outline
report.status = ReportStatus.PLANNING
ReportManager.update_progress(report_id, "planning", 5, t('progress.startPlanningOutline'), completed_sections=[])
self.report_logger.log_planning_start()
if progress_callback:
progress_callback("planning", 0, t('progress.startPlanningOutline'))
outline = self.plan_outline(
progress_callback=lambda stage, prog, msg:
progress_callback(stage, prog // 5, msg) if progress_callback else None,
)
report.outline = outline
self.report_logger.log_planning_complete(outline.to_dict())
ReportManager.save_outline(report_id, outline)
ReportManager.update_progress(
report_id, "planning", 15,
t('progress.outlineDone', count=len(outline.sections)),
completed_sections=[],
)
ReportManager.save_report(report)
# Pre-compute infographic data
try:
infographic_data = self.analytics.get_infographic_data(self.simulation_id)
ReportManager.save_infographic(report_id, infographic_data)
logger.info(f"Infographic data saved for {report_id}")
except Exception as e:
logger.warning(f"Failed to pre-compute infographic: {e}")
# Phase 2: Generate sections
report.status = ReportStatus.GENERATING
total_sections = len(outline.sections)
generated_sections = []
for i, section in enumerate(outline.sections):
section_num = i + 1
base_progress = 20 + int((i / total_sections) * 70)
ReportManager.update_progress(
report_id, "generating", base_progress,
t('progress.generatingSection', title=section.title, current=section_num, total=total_sections),
current_section=section.title,
completed_sections=completed_section_titles,
)
if progress_callback:
progress_callback("generating", base_progress,
t('progress.generatingSection', title=section.title, current=section_num, total=total_sections))
section_content = self._generate_section_react(
section=section,
outline=outline,
previous_sections=generated_sections,
progress_callback=lambda stage, prog, msg:
progress_callback(stage, base_progress + int(prog * 0.7 / total_sections), msg)
if progress_callback else None,
section_index=section_num,
)
section.content = section_content
generated_sections.append(f"## {section.title}\n\n{section_content}")
ReportManager.save_section(report_id, section_num, section)
completed_section_titles.append(section.title)
full_section = f"## {section.title}\n\n{section_content}"
if self.report_logger:
self.report_logger.log_section_full_complete(section.title, section_num, full_section.strip())
if section_num < total_sections:
time.sleep(3)
# Phase 3: Assemble
if progress_callback:
progress_callback("generating", 95, t('progress.assemblingReport'))
report.markdown_content = ReportManager.assemble_full_report(report_id, outline)
report.status = ReportStatus.COMPLETED
report.completed_at = time.strftime('%Y-%m-%dT%H:%M:%S')
total_time = time.time() - start_time
if self.report_logger:
self.report_logger.log_report_complete(total_sections, total_time)
ReportManager.save_report(report)
ReportManager.update_progress(
report_id, "completed", 100, t('progress.reportComplete'),
completed_sections=completed_section_titles,
)
if progress_callback:
progress_callback("completed", 100, t('progress.reportComplete'))
if self.console_logger:
self.console_logger.close()
self.console_logger = None
return report
except Exception as e:
logger.error(t('report.reportGenFailed', error=str(e)))
report.status = ReportStatus.FAILED
report.error = str(e)
if self.report_logger:
self.report_logger.log_error(str(e), "failed")
try:
ReportManager.save_report(report)
ReportManager.update_progress(
report_id, "failed", -1,
t('progress.reportFailed', error=str(e)),
completed_sections=completed_section_titles,
)
except Exception:
pass
if self.console_logger:
self.console_logger.close()
self.console_logger = None
return report
# ── Chat ──
def chat(self, message: str, chat_history: List[Dict[str, str]] = None) -> Dict[str, Any]:
logger.info(t('report.agentChat', message=message[:50]))
chat_history = chat_history or []
report_content = ""
try:
report = ReportManager.get_report_by_simulation(self.simulation_id)
if report and report.markdown_content:
report_content = report.markdown_content[:15000]
if len(report.markdown_content) > 15000:
report_content += "\n\n... [报告内容已截断] ..."
except Exception as e:
logger.warning(t('report.fetchReportFailed', error=e))
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(),
)
system_prompt = f"{system_prompt}\n\n{get_language_instruction()}"
messages = [{"role": "system", "content": system_prompt}]
for h in chat_history[-10:]:
messages.append(h)
messages.append({"role": "user", "content": message})
tool_calls_made = []
max_iterations = 2
for iteration in range(max_iterations):
response = self.llm.chat(messages=messages, temperature=0.5)
tool_calls = self._parse_tool_calls(response)
if not tool_calls:
clean = re.sub(r'edisnormal.*?edisnormal', '', response, flags=re.DOTALL)
return {
"response": clean.strip(),
"tool_calls": tool_calls_made,
"sources": [tc.get("parameters", {}).get("query", "") for tc in tool_calls_made],
}
tool_results = []
for call in tool_calls[:1]:
if len(tool_calls_made) >= self.MAX_TOOL_CALLS_PER_CHAT:
break
result = self._execute_tool(call["name"], call.get("parameters", {}))
tool_results.append({"tool": call["name"], "result": result[:1500]})
tool_calls_made.append(call)
messages.append({"role": "assistant", "content": response})
observation = "\n".join(f"[{r['tool']}结果]\n{r['result']}" for r in tool_results)
messages.append({"role": "user", "content": observation + CHAT_OBSERVATION_SUFFIX})
final_response = self.llm.chat(messages=messages, temperature=0.5)
clean = re.sub(r'edisnormal.*?edisnormal', '', final_response, flags=re.DOTALL)
return {
"response": clean.strip(),
"tool_calls": tool_calls_made,
"sources": [tc.get("parameters", {}).get("query", "") for tc in tool_calls_made],
}