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
84 changes: 84 additions & 0 deletions backend/app/api/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from . import report_bp
from ..config import Config
from ..services.report_agent import ReportAgent, ReportManager, ReportStatus
from ..services.signal_extractor import SignalExtractor
from ..services.simulation_manager import SimulationManager
from ..models.project import ProjectManager
from ..models.task import TaskManager, TaskStatus
Expand Down Expand Up @@ -925,6 +926,89 @@ def stream_console_log(report_id: str):
}), 500


# ============== 预测信号接口 ==============

@report_bp.route('/<report_id>/signal', methods=['POST'])
def extract_signal(report_id: str):
"""
从已完成的报告中提取结构化预测信号(miro_signal)

对报告的 markdown 内容执行一次 LLM 提取,返回可供
外部预测市场管道直接消费的规范化概率信号。

返回:
{
"success": true,
"data": {
"signal_id": "uuid",
"schema_version": "1.1",
"report_id": "report_xxxx",
"simulation_id": "sim_xxxx",
"generated_at": "2026-...",
"thesis": {
"p_yes": 0.73,
"confidence": "high",
"action": "buy_yes",
"regime": "consensus_forming",
"summary": "...",
"drivers": ["...", "..."],
"invalidators": ["...", "..."]
}
}
}
"""
try:
report = ReportManager.get_report(report_id)

if not report:
return jsonify({
"success": False,
"error": f"报告不存在: {report_id}"
}), 404

if report.status != ReportStatus.COMPLETED:
return jsonify({
"success": False,
"error": f"报告尚未完成 (status={report.status.value}),无法提取信号"
}), 400

if not report.markdown_content:
return jsonify({
"success": False,
"error": "报告内容为空,无法提取信号"
}), 400

extractor = SignalExtractor()
signal = extractor.extract(
report_id=report_id,
simulation_id=report.simulation_id,
markdown_content=report.markdown_content,
simulation_requirement=report.simulation_requirement,
)

logger.info(f"信号提取完成: report={report_id} p_yes={signal.p_yes} action={signal.action}")

return jsonify({
"success": True,
"data": signal.to_dict()
})

except ValueError as e:
logger.error(f"信号提取失败 (LLM): {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 422

except Exception as e:
logger.error(f"信号提取失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500


# ============== 工具调用接口(供调试使用)==============

@report_bp.route('/tools/search', methods=['POST'])
Expand Down
96 changes: 34 additions & 62 deletions backend/app/services/oasis_profile_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
from dataclasses import dataclass, field
from datetime import datetime

from openai import OpenAI
from zep_cloud.client import Zep

from ..config import Config
from ..utils.llm_client import LLMClient
from ..utils.logger import get_logger
from .zep_entity_reader import EntityNode, ZepEntityReader

Expand Down Expand Up @@ -192,9 +192,10 @@ def __init__(
if not self.api_key:
raise ValueError("LLM_API_KEY 未配置")

self.client = OpenAI(
self.llm_client = LLMClient(
api_key=self.api_key,
base_url=self.base_url
base_url=self.base_url,
model=self.model_name
)

# Zep客户端用于检索丰富上下文
Expand Down Expand Up @@ -520,64 +521,36 @@ def _generate_profile_with_llm(
entity_name, entity_type, entity_summary, entity_attributes, context
)

# 尝试多次生成,直到成功或达到最大重试次数
max_attempts = 3
last_error = None

for attempt in range(max_attempts):
try:
response = self.client.chat.completions.create(
model=self.model_name,
messages=[
{"role": "system", "content": self._get_system_prompt(is_individual)},
{"role": "user", "content": prompt}
],
response_format={"type": "json_object"},
temperature=0.7 - (attempt * 0.1) # 每次重试降低温度
# 不设置max_tokens,让LLM自由发挥
)

content = response.choices[0].message.content

# 检查是否被截断(finish_reason不是'stop')
finish_reason = response.choices[0].finish_reason
if finish_reason == 'length':
logger.warning(f"LLM输出被截断 (attempt {attempt+1}), 尝试修复...")
content = self._fix_truncated_json(content)

# 尝试解析JSON
try:
result = json.loads(content)

# 验证必需字段
if "bio" not in result or not result["bio"]:
result["bio"] = entity_summary[:200] if entity_summary else f"{entity_type}: {entity_name}"
if "persona" not in result or not result["persona"]:
result["persona"] = entity_summary or f"{entity_name}是一个{entity_type}。"

return result

except json.JSONDecodeError as je:
logger.warning(f"JSON解析失败 (attempt {attempt+1}): {str(je)[:80]}")

# 尝试修复JSON
result = self._try_fix_json(content, entity_name, entity_type, entity_summary)
if result.get("_fixed"):
del result["_fixed"]
return result

last_error = je

except Exception as e:
logger.warning(f"LLM调用失败 (attempt {attempt+1}): {str(e)[:80]}")
last_error = e
import time
time.sleep(1 * (attempt + 1)) # 指数退避

logger.warning(f"LLM生成人设失败({max_attempts}次尝试): {last_error}, 使用规则生成")
return self._generate_profile_rule_based(
entity_name, entity_type, entity_summary, entity_attributes
)
try:
result = self.llm_client.chat_json(
messages=[
{"role": "system", "content": self._get_system_prompt(is_individual)},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=None,
max_attempts=3,
temperature_step=0.1,
fallback_parser=lambda content: self._try_fix_json(
content, entity_name, entity_type, entity_summary
),
retry_delay_seconds=1.0
)

if "bio" not in result or not result["bio"]:
result["bio"] = entity_summary[:200] if entity_summary else f"{entity_type}: {entity_name}"
if "persona" not in result or not result["persona"]:
result["persona"] = entity_summary or f"{entity_name}是一个{entity_type}。"

if result.get("_fixed"):
del result["_fixed"]

return result
except Exception as e:
logger.warning(f"LLM生成人设失败(3次尝试): {e}, 使用规则生成")
return self._generate_profile_rule_based(
entity_name, entity_type, entity_summary, entity_attributes
)

def _fix_truncated_json(self, content: str) -> str:
"""修复被截断的JSON(输出被max_tokens限制截断)"""
Expand Down Expand Up @@ -1197,4 +1170,3 @@ def save_profiles_to_json(
"""[已废弃] 请使用 save_profiles() 方法"""
logger.warning("save_profiles_to_json已废弃,请使用save_profiles方法")
self.save_profiles(profiles, file_path, platform)

Loading