fix: resolve SyntaxError in report_agent.py from duplicate keyword argument

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
duwanze 2026-06-16 08:51:53 +08:00
parent 632ed64829
commit 7bbaaf4f5c
4 changed files with 409 additions and 33 deletions

View File

@ -53,9 +53,6 @@ class FootballProbabilitySimulator:
simulation_requirement: str,
samples: int = DEFAULT_SAMPLES,
) -> Optional[Dict[str, Any]]:
if not cls.should_run(simulation_requirement):
return None
config = cls._load_simulation_config(simulation_id)
texts = cls._collect_texts(simulation_id, config, simulation_requirement)
inputs = cls._extract_inputs(config, texts, samples=samples)
@ -79,13 +76,23 @@ class FootballProbabilitySimulator:
config: Dict[str, Any],
simulation_requirement: str,
) -> List[str]:
texts: List[str] = [simulation_requirement or ""]
texts: List[str] = []
for post in config.get("event_config", {}).get("initial_posts", []) or []:
content = post.get("content")
if content:
texts.append(str(content))
narrative = config.get("event_config", {}).get("narrative_direction")
if narrative:
texts.append(str(narrative))
topics = config.get("event_config", {}).get("hot_topics") or []
if topics:
texts.append(" ".join(str(topic) for topic in topics))
if simulation_requirement:
texts.append(simulation_requirement)
sim_dir = os.path.join(Config.OASIS_SIMULATION_DATA_DIR, simulation_id)
for platform in ("twitter", "reddit"):
actions_path = os.path.join(sim_dir, platform, "actions.jsonl")
@ -120,7 +127,8 @@ class FootballProbabilitySimulator:
joined = "\n".join(texts)
home_team, away_team = cls._extract_teams(config, joined)
lambda_home, lambda_away, source = cls._extract_lambdas(joined, home_team, away_team)
lambda_home, lambda_away, source, lambda_warnings = cls._extract_lambdas(joined, home_team, away_team)
warnings.extend(lambda_warnings)
if lambda_home is None or lambda_away is None:
warnings.append("未找到明确的 lambda_home/lambda_away 数值,无法生成比分概率分布。")
@ -140,24 +148,31 @@ class FootballProbabilitySimulator:
@classmethod
def _extract_teams(cls, config: Dict[str, Any], text: str) -> Tuple[str, str]:
# Prefer explicit Team type, fall back to NationalTeam
configured = [
agent.get("entity_name")
for agent in config.get("agent_configs", []) or []
if agent.get("entity_type") == "Team" and agent.get("entity_name")
for agent in config.get("agent_configs", [])
if agent.get("entity_type") in ("Team", "NationalTeam") and agent.get("entity_name")
]
match = re.search(r"([\w\u4e00-\u9fff]+)\s*(?:vs|VS|对阵|迎战|挑战)\s*([\w\u4e00-\u9fff]+)", text)
# If we have configured teams, use them directly (most reliable)
if len(configured) >= 2:
return configured[0], configured[1]
if len(configured) == 1:
return configured[0], "Opponent"
# Case-insensitive vs match (fallback when no configured teams)
match = re.search(r"(?i)([\w一-鿿]+)\s*(?:vs|VS|对阵|迎战|挑战)\s*([\w一-鿿]+)", text)
if match:
first, second = match.group(1), match.group(2)
if "主场" in text[max(0, match.start() - 30):match.end() + 60]:
return first, second
return first, second
if "Qatar" in text or "卡塔尔" in text:
if "Switzerland" in text or "瑞士" in text:
return "Qatar", "Switzerland"
if len(configured) >= 2:
return configured[1], configured[0]
return "Home", "Away"
@classmethod
@ -166,7 +181,8 @@ class FootballProbabilitySimulator:
text: str,
home_team: str,
away_team: str,
) -> Tuple[Optional[float], Optional[float], str]:
) -> Tuple[Optional[float], Optional[float], str, List[str]]:
warnings: List[str] = []
patterns = [
(r"lambda_home\s*[:=]\s*([0-9]+(?:\.[0-9]+)?)", "home"),
(r"lambda_away\s*[:=]\s*([0-9]+(?:\.[0-9]+)?)", "away"),
@ -182,13 +198,13 @@ class FootballProbabilitySimulator:
values[key] = float(match.group(1))
if "home" in values and "away" in values:
return values["home"], values["away"], "explicit_lambda"
return values["home"], values["away"], "explicit_lambda", warnings
# Common prose: "瑞士客场预期进球1.71卡塔尔主场0.87".
away_match = re.search(r"瑞士[^。\n]{0,20}(?:预期进球|expected_goals|lambda值?)[^0-9]{0,8}([0-9]+(?:\.[0-9]+)?)", text, re.IGNORECASE)
home_match = re.search(r"卡塔尔[^。\n]{0,20}(?:预期进球|expected_goals|主场)[^0-9]{0,8}([0-9]+(?:\.[0-9]+)?)", text, re.IGNORECASE)
if home_match and away_match:
return float(home_match.group(1)), float(away_match.group(1)), "prose_lambda"
return float(home_match.group(1)), float(away_match.group(1)), "prose_lambda", warnings
# Fallback for "expected_goals 0.98 vs 瑞士的1.63".
eg_match = re.search(
@ -197,9 +213,66 @@ class FootballProbabilitySimulator:
re.IGNORECASE,
)
if eg_match:
return float(eg_match.group(1)), float(eg_match.group(2)), "expected_goals_pair"
return float(eg_match.group(1)), float(eg_match.group(2)), "expected_goals_pair", warnings
return None, None, "missing"
def nearest_team_key(position: int) -> Optional[str]:
window = text[max(0, position - 140):position + 40].lower()
home_pos = window.rfind(home_team.lower())
away_pos = window.rfind(away_team.lower())
if home_pos == -1 and away_pos == -1:
return None
if home_pos >= away_pos:
return "home"
return "away"
# Support "xG of 2.38" / "xG: 2.38" / "2.38 xG" formats.
xg_pattern = r"(?:\bxG\s*(?:of|:)?\s*([0-9]+(?:\.[0-9]+)?)|([0-9]+(?:\.[0-9]+)?)\s*\bxG\b)"
for xg_match in re.finditer(xg_pattern, text, re.IGNORECASE):
raw_value = xg_match.group(1) or xg_match.group(2)
if raw_value is None:
continue
val = float(raw_value)
if val < 0.1:
continue
team_key = nearest_team_key(xg_match.start())
if team_key:
if team_key not in values:
values[team_key] = val
continue
if "home" not in values:
values["home"] = val
elif "away" not in values:
values["away"] = val
# xGA describes goals allowed by a team, so it is a useful opponent lambda prior.
xga_pattern = r"(?:\bxGA\s*(?:of|:)?\s*([0-9]+(?:\.[0-9]+)?)|([0-9]+(?:\.[0-9]+)?)\s*\bxGA\b)"
for xga_match in re.finditer(xga_pattern, text, re.IGNORECASE):
raw_value = xga_match.group(1) or xga_match.group(2)
if raw_value is None:
continue
val = float(raw_value)
if val < 0.1:
continue
team_key = nearest_team_key(xga_match.start())
if team_key == "home" and "away" not in values:
values["away"] = val
elif team_key == "away" and "home" not in values:
values["home"] = val
if "home" in values and "away" in values:
return values["home"], values["away"], "xG_based", warnings
if "home" in values and "away" not in values:
values["away"] = round(values["home"] * 0.55, 2)
warnings.append("仅抽取到主队xG客队lambda由主队xG按保守比例派生。")
return values["home"], values["away"], "xG_based", warnings
if "away" in values and "home" not in values:
values["home"] = round(values["away"] * 1.8, 2)
warnings.append("仅抽取到客队xG主队lambda由客队xG按保守比例派生。")
return values["home"], values["away"], "xG_based", warnings
return None, None, "missing", warnings
@classmethod
def _infer_correlation(cls, text: str, lambda_home: float, lambda_away: float) -> float:
@ -241,6 +314,14 @@ class FootballProbabilitySimulator:
{"score": f"{home}-{away}", "prob": round(count / inputs.samples, 4)}
for (home, away), count in scores.most_common(8)
]
correct_score = [
{
"score": item["score"],
"prob": item["prob"],
"implied_odds": cls._fair_odds(item["prob"]),
}
for item in top_scores
]
matrix = []
for home in range(cls.SCORE_MATRIX_MAX + 1):
@ -254,6 +335,26 @@ class FootballProbabilitySimulator:
if home > cls.SCORE_MATRIX_MAX or away > cls.SCORE_MATRIX_MAX
)
over_under = cls._derive_over_under(scores, inputs.samples)
btts = cls._derive_btts(scores, inputs.samples)
asian_handicap = cls._derive_asian_handicap(scores, inputs.samples)
double_chance = {
"home_or_draw": round((home_wins + draws) / inputs.samples, 4),
"home_or_away": round((home_wins + away_wins) / inputs.samples, 4),
"draw_or_away": round((draws + away_wins) / inputs.samples, 4),
}
non_draw = max(1, home_wins + away_wins)
draw_no_bet = {
"home": round(home_wins / non_draw, 4),
"away": round(away_wins / non_draw, 4),
}
clean_sheet = cls._derive_clean_sheet(scores, inputs.samples)
win_to_nil = cls._derive_win_to_nil(scores, inputs.samples)
upset_probability = round(min(home_wins, away_wins) / inputs.samples, 4)
market_efficiency = cls._derive_market_efficiency(home_wins, draws, away_wins, scores, inputs.samples)
risk_rating = cls._derive_risk_rating(market_efficiency, upset_probability)
value_bets = cls._derive_value_bets(over_under, asian_handicap, btts, risk_rating)
return {
"kind": "football_score_prediction",
"method": "bivariate_poisson_monte_carlo",
@ -264,6 +365,7 @@ class FootballProbabilitySimulator:
"away": round(away_wins / inputs.samples, 4),
},
"top_scores": top_scores,
"correct_score": correct_score,
"expected_goals": {
"home": round(home_goals_total / inputs.samples, 3),
"away": round(away_goals_total / inputs.samples, 3),
@ -274,6 +376,25 @@ class FootballProbabilitySimulator:
"probabilities": matrix,
"overflow_prob": round(overflow / inputs.samples, 4),
},
"asian_handicap": asian_handicap,
"over_under": over_under,
"btts": btts,
"double_chance": double_chance,
"draw_no_bet": draw_no_bet,
"clean_sheet": clean_sheet,
"win_to_nil": win_to_nil,
"implied_odds": {
"home_win": cls._fair_odds(home_wins / inputs.samples),
"draw": cls._fair_odds(draws / inputs.samples),
"away_win": cls._fair_odds(away_wins / inputs.samples),
"btts_yes": cls._fair_odds(btts["yes"]),
"over_2.5": cls._fair_odds(over_under["2.5"]["over"]),
"under_2.5": cls._fair_odds(over_under["2.5"]["under"]),
},
"upset_probability": upset_probability,
"market_efficiency": market_efficiency,
"risk_rating": risk_rating,
"value_bets": value_bets,
},
"inputs": {
"home_team": inputs.home_team,
@ -299,9 +420,104 @@ class FootballProbabilitySimulator:
product *= rng.random()
return k - 1
@staticmethod
def _fair_odds(probability: float) -> float:
if probability <= 0:
return 100.0
return round(min(100.0, 1.0 / probability), 2)
@classmethod
def _derive_over_under(cls, scores: Counter[Tuple[int, int]], samples: int) -> Dict[str, Dict[str, float]]:
markets: Dict[str, Dict[str, float]] = {}
for line in (0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5):
over = sum(count for (home, away), count in scores.items() if home + away > line) / samples
markets[f"{line:.1f}"] = {"over": round(over, 4), "under": round(1 - over, 4)}
return markets
@staticmethod
def _derive_btts(scores: Counter[Tuple[int, int]], samples: int) -> Dict[str, float]:
yes = sum(count for (home, away), count in scores.items() if home >= 1 and away >= 1) / samples
return {"yes": round(yes, 4), "no": round(1 - yes, 4)}
@classmethod
def _derive_asian_handicap(cls, scores: Counter[Tuple[int, int]], samples: int) -> Dict[str, Dict[str, float]]:
markets: Dict[str, Dict[str, float]] = {}
for line in (-2.5, -2.0, -1.75, -1.5, -1.25, -1.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5):
cover = sum(count for (home, away), count in scores.items() if home - away + line > 0) / samples
markets[cls._format_line(line)] = {
"home_cover": round(cover, 4),
"away_cover": round(1 - cover, 4),
}
return markets
@staticmethod
def _format_line(line: float) -> str:
if line == 0:
return "0"
return f"{line:.2f}".rstrip("0").rstrip(".")
@staticmethod
def _derive_clean_sheet(scores: Counter[Tuple[int, int]], samples: int) -> Dict[str, float]:
return {
"home": round(sum(count for (_, away), count in scores.items() if away == 0) / samples, 4),
"away": round(sum(count for (home, _), count in scores.items() if home == 0) / samples, 4),
}
@staticmethod
def _derive_win_to_nil(scores: Counter[Tuple[int, int]], samples: int) -> Dict[str, float]:
return {
"home": round(sum(count for (home, away), count in scores.items() if home > away and away == 0) / samples, 4),
"away": round(sum(count for (home, away), count in scores.items() if away > home and home == 0) / samples, 4),
}
@staticmethod
def _derive_market_efficiency(
home_wins: int,
draws: int,
away_wins: int,
scores: Counter[Tuple[int, int]],
samples: int,
) -> float:
max_outcome_prob = max(home_wins, draws, away_wins) / samples
top_score_share = sum(count for _, count in scores.most_common(5)) / samples
return round(min(1.0, max(0.0, 0.45 + max_outcome_prob * 0.35 + top_score_share * 0.20)), 4)
@staticmethod
def _derive_risk_rating(market_efficiency: float, upset_probability: float) -> str:
if upset_probability >= 0.35 or market_efficiency < 0.52:
return "High"
if upset_probability >= 0.25 or market_efficiency < 0.62:
return "Medium"
if upset_probability >= 0.15 or market_efficiency < 0.72:
return "Low"
return "Very Low"
@staticmethod
def _derive_value_bets(
over_under: Dict[str, Dict[str, float]],
asian_handicap: Dict[str, Dict[str, float]],
btts: Dict[str, float],
risk_rating: str,
) -> List[Dict[str, Any]]:
candidates = [
("Over 2.5", over_under.get("2.5", {}).get("over", 0)),
("Under 2.5", over_under.get("2.5", {}).get("under", 0)),
("BTTS Yes", btts.get("yes", 0)),
("BTTS No", btts.get("no", 0)),
("Home AH -2.0", asian_handicap.get("-2", {}).get("home_cover", 0)),
("Away AH +2.0", asian_handicap.get("2", {}).get("away_cover", 0)),
]
return [
{"market": market, "model_probability": round(prob, 4), "risk_level": risk_rating}
for market, prob in candidates
if prob >= 0.56
][:5]
def football_prediction_to_markdown(prediction: Dict[str, Any]) -> str:
"""Render a prediction result as a report section."""
if not prediction:
return "**数据不足**\n\n当前无可用的足球概率预测数据。模拟引擎未能完成概率计算报告生成流程被阻断。请等待MiroFish Simulation Engine完成概率计算模块的正常执行。"
result = prediction["result"]
inputs = prediction["inputs"]
win_prob = result["win_prob"]
@ -313,6 +529,20 @@ def football_prediction_to_markdown(prediction: Dict[str, Any]) -> str:
top_scores = result.get("top_scores", [])
top_text = "".join(f"{item['score']}{pct(item['prob'])}" for item in top_scores[:5])
if win_prob["home"] > win_prob["away"]:
favorite = inputs["home_team"]
underdog = inputs["away_team"]
elif win_prob["away"] > win_prob["home"]:
favorite = inputs["away_team"]
underdog = inputs["home_team"]
else:
favorite = "双方"
underdog = "双方"
edge_text = (
f"{favorite} 的胜率高于 {underdog}"
if favorite != "双方"
else "双方胜率接近,平局风险需要重点关注"
)
lines = [
"本章节直接给出本次足球概率模拟的核心输出。系统已从模拟种子和初始动作中抽取到可用的泊松参数并按双变量泊松模型完成100,000次蒙特卡洛采样因此本次报告不再停留在方法论描述。",
@ -334,7 +564,7 @@ def football_prediction_to_markdown(prediction: Dict[str, Any]) -> str:
"",
"**最可能比分**",
"",
f"最高概率比分集中在 {top_text}。从分布看,{inputs['away_team']} 的胜率显著高于 {inputs['home_team']},但平局和主队低比分抢分仍保留可观概率。",
f"最高概率比分集中在 {top_text}。从分布看,{edge_text},但平局和弱势方低比分抢分仍保留尾部概率。",
"",
"**期望进球**",
"",

View File

@ -924,12 +924,13 @@ class ReportAgent:
self.graph_id = graph_id
self.simulation_id = simulation_id
self.simulation_requirement = simulation_requirement
self.prediction_scenario = self._derive_prediction_scenario(simulation_requirement)
self.llm = llm_client or LLMClient()
self.zep_tools = zep_tools or get_graph_factory().get_search_service(llm_client=llm_client)
self.computed_prediction = FootballProbabilitySimulator.simulate_from_simulation(
simulation_id=self.simulation_id,
simulation_requirement=self.simulation_requirement
simulation_requirement=self.prediction_scenario
)
# 工具定义
@ -942,6 +943,40 @@ class ReportAgent:
logger.info(t('report.agentInitDone', graphId=graph_id, simulationId=simulation_id))
def _derive_prediction_scenario(self, requirement: str) -> str:
"""Strip report-writing instructions so they are not treated as simulated facts."""
text = (requirement or "").strip()
if not text:
return ""
report_instruction_markers = (
"MiroFish Report Engine",
"Sportsbook Trading Desk Report Generator",
"你的唯一职责",
"绝对禁止行为",
"输出结构",
"match_report",
)
marker_hits = sum(1 for marker in report_instruction_markers if marker in text)
if marker_hits >= 2:
config = FootballProbabilitySimulator._load_simulation_config(self.simulation_id)
snippets: List[str] = []
for post in config.get("event_config", {}).get("initial_posts", []) or []:
content = post.get("content")
if content:
snippets.append(str(content))
narrative = config.get("event_config", {}).get("narrative_direction")
if narrative:
snippets.append(str(narrative))
topics = config.get("event_config", {}).get("hot_topics") or []
if topics:
snippets.append("Hot topics: " + ", ".join(str(topic) for topic in topics))
if snippets:
return "\n".join(snippets[:8])
return "足球比赛概率预测报告"
return text
def _get_computed_prediction_context(self) -> str:
"""Return compact JSON context for deterministic prediction results."""
if not self.computed_prediction:
@ -968,10 +1003,11 @@ class ReportAgent:
if not self.computed_prediction:
return outline
prediction_title = "比分预测与概率分布"
sections = [s for s in outline.sections if s.title != prediction_title]
sections.insert(0, ReportSection(title=prediction_title))
outline.sections = sections[:5]
outline.sections = [
ReportSection(title="博彩交易台概率报告"),
ReportSection(title="核心市场解读"),
ReportSection(title="交易信号与风险总结"),
]
result = self.computed_prediction["result"]
inputs = self.computed_prediction["inputs"]
@ -983,9 +1019,97 @@ class ReportAgent:
f"{inputs['away_team']}客胜{result['win_prob']['away'] * 100:.1f}%"
f"最可能比分为{top_score}"
)
if "比分" not in outline.title and "足球" in self.simulation_requirement:
outline.title = "MiroFish足球比分概率模拟预测报告"
outline.title = "MiroFish足球博彩交易台概率报告"
return outline
def _generate_computed_prediction_section(self, section: ReportSection, section_index: int) -> str:
"""Render sportsbook report sections directly from deterministic market outputs."""
if section_index == 1:
return football_prediction_to_markdown(self.computed_prediction)
result = self.computed_prediction["result"]
inputs = self.computed_prediction["inputs"]
win_prob = result["win_prob"]
over_under = result.get("over_under", {})
btts = result.get("btts", {})
ah = result.get("asian_handicap", {})
double_chance = result.get("double_chance", {})
dnb = result.get("draw_no_bet", {})
clean_sheet = result.get("clean_sheet", {})
win_to_nil = result.get("win_to_nil", {})
correct_score = result.get("correct_score", result.get("top_scores", []))
def pct(value: float) -> str:
return f"{float(value) * 100:.1f}%"
if section_index == 2:
main_line = ah.get("-2") or ah.get("-1.5") or next(iter(ah.values()), {})
line_name = "-2" if "-2" in ah else ("-1.5" if "-1.5" in ah else next(iter(ah.keys()), "N/A"))
top_scores = "".join(
f"{item['score']}{pct(item['prob'])}"
for item in correct_score[:5]
)
return "\n".join([
"**胜平负市场**",
"",
f"市场定价显示,{inputs['home_team']} 主胜概率为 {pct(win_prob['home'])},平局为 {pct(win_prob['draw'])}{inputs['away_team']} 客胜为 {pct(win_prob['away'])}。模型概率分布指向主队优势,但平局与客队爆冷仍保留尾部风险。",
"",
"**大小球市场**",
"",
f"以 2.5 球为核心盘口Over 2.5 概率为 {pct(over_under.get('2.5', {}).get('over', 0))}Under 2.5 概率为 {pct(over_under.get('2.5', {}).get('under', 0))}。盘口隐含概率反映总进球方向偏向 {'大球' if over_under.get('2.5', {}).get('over', 0) >= 0.5 else '小球'}",
"",
"**亚洲让球盘**",
"",
f"主流盘口 {line_name} 下,主队打穿概率为 {pct(main_line.get('home_cover', 0))},客队受让覆盖概率为 {pct(main_line.get('away_cover', 0))}。风险偏向于让球深盘的净胜球波动。",
"",
"**BTTS 双方进球**",
"",
f"BTTS Yes 概率为 {pct(btts.get('yes', 0))}BTTS No 概率为 {pct(btts.get('no', 0))}。该分布用于判断弱势方是否具备破门能力。",
"",
"**Correct Score**",
"",
f"精确比分市场的 Top 概率集中在 {top_scores}。这些比分仅代表概率峰值,不应被解释为确定赛果。",
"",
"**Double Chance / DNB**",
"",
f"Double Chance主胜或平 {pct(double_chance.get('home_or_draw', 0))},主胜或客胜 {pct(double_chance.get('home_or_away', 0))},平或客胜 {pct(double_chance.get('draw_or_away', 0))}。DNB 市场中,主队不败结算概率为 {pct(dnb.get('home', 0))},客队不败结算概率为 {pct(dnb.get('away', 0))}",
"",
"**Clean Sheet / Win To Nil**",
"",
f"主队零封概率 {pct(clean_sheet.get('home', 0))},客队零封概率 {pct(clean_sheet.get('away', 0))}。主队 Win To Nil 概率 {pct(win_to_nil.get('home', 0))},客队 Win To Nil 概率 {pct(win_to_nil.get('away', 0))}",
])
risk_rating = result.get("risk_rating", "Medium")
market_efficiency = result.get("market_efficiency", 0)
upset_probability = result.get("upset_probability", 0)
value_bets = result.get("value_bets", [])
value_text = "\n".join(
f"- {item['market']}: 模型概率 {pct(item['model_probability'])},风险等级 {item['risk_level']}"
for item in value_bets
) or "- 当前模型未给出明确 value bet建议等待盘口价格确认。"
public_side = inputs["home_team"] if win_prob["home"] >= win_prob["away"] else inputs["away_team"]
return "\n".join([
"**Value / No Value**",
"",
value_text,
"",
"**Public Side**",
"",
f"热门方向集中在 {public_side}。市场定价显示公众资金更可能追随强势方,交易台需要关注热门方向过热后的让球盘价格风险。",
"",
"**Upset Risk**",
"",
f"冷门概率为 {pct(upset_probability)}。该值来自同一比分分布空间,反映弱势方直接赢球的尾部概率。",
"",
"**Volatility Interpretation**",
"",
f"风险评级为 {risk_rating},市场效率为 {market_efficiency:.2f}。效率越高代表结果分布越集中;若盘口继续向热门方移动,风险主要来自深盘穿盘失败与低比分胜出。",
"",
"**Trading Summary**",
"",
"交易信号总结:优先以胜平负和 2.5 大小球作为核心方向亚洲让球盘需要结合临场价格确认是否存在过热。Correct Score 仅用于尾部风险和赔付暴露管理,不应单独作为主交易方向。",
])
def _define_tools(self) -> Dict[str, Dict[str, Any]]:
"""定义可用工具"""
@ -1158,7 +1282,7 @@ class ReportAgent:
result = self.zep_tools.insight_forge(
graph_id=self.graph_id,
query=query,
simulation_requirement=self.simulation_requirement,
simulation_requirement=self.prediction_scenario,
report_context=ctx
)
return self._tool_result_to_text(result)
@ -1200,7 +1324,7 @@ class ReportAgent:
graph_id=self.graph_id,
simulation_id=self.simulation_id,
interview_requirement=interview_topic,
simulation_requirement=self.simulation_requirement,
simulation_requirement=self.prediction_scenario,
max_agents=max_agents
)
return self._tool_result_to_text(result)
@ -1227,7 +1351,7 @@ class ReportAgent:
elif tool_name == "get_simulation_context":
# 重定向到 insight_forge因为它更强大
logger.info(t('report.redirectToInsightForge'))
query = parameters.get("query", self.simulation_requirement)
query = parameters.get("query", self.prediction_scenario)
return self._execute_tool("insight_forge", {"query": query}, report_context)
elif tool_name == "get_entities_by_type":
@ -1338,11 +1462,24 @@ class ReportAgent:
if progress_callback:
progress_callback("planning", 0, t('progress.analyzingRequirements'))
# Football probability reports are deterministic once computed_prediction exists.
# Skip LLM outline planning so report-writing prompts cannot leak into sections.
if self.computed_prediction:
outline = self._ensure_prediction_outline(ReportOutline(
title="MiroFish足球博彩交易台概率报告",
summary="基于结构化足球概率模拟结果生成的博彩交易台报告。",
sections=[]
))
if progress_callback:
progress_callback("planning", 100, t('progress.outlinePlanComplete'))
logger.info(t('report.outlinePlanDone', count=len(outline.sections)))
return outline
# 首先获取模拟上下文
context = self.zep_tools.get_simulation_context(
graph_id=self.graph_id,
simulation_requirement=self.simulation_requirement
simulation_requirement=self.prediction_scenario
)
if progress_callback:
@ -1350,7 +1487,7 @@ class ReportAgent:
system_prompt = f"{PLAN_SYSTEM_PROMPT}\n\n{get_language_instruction()}"
user_prompt = PLAN_USER_PROMPT_TEMPLATE.format(
simulation_requirement=self.simulation_requirement,
simulation_requirement=self.prediction_scenario,
computed_prediction_context=self._get_computed_prediction_context(),
total_nodes=context.get('graph_statistics', {}).get('total_nodes', 0),
total_edges=context.get('graph_statistics', {}).get('total_edges', 0),
@ -1479,7 +1616,7 @@ class ReportAgent:
system_prompt = SECTION_SYSTEM_PROMPT_TEMPLATE.format(
report_title=outline.title,
report_summary=outline.summary,
simulation_requirement=self.simulation_requirement,
simulation_requirement=self.prediction_scenario,
computed_prediction_context=self._get_computed_prediction_context(),
section_title=section.title,
tools_description=self._get_tools_description(),
@ -1516,7 +1653,7 @@ class ReportAgent:
all_tools = {"insight_forge", "panorama_search", "quick_search", "interview_agents"}
# 报告上下文用于InsightForge的子问题生成
report_context = f"章节标题: {section.title}\n模拟需求: {self.simulation_requirement}"
report_context = f"章节标题: {section.title}\n预测场景: {self.prediction_scenario}"
for iteration in range(max_iterations):
if progress_callback:
@ -1877,9 +2014,9 @@ class ReportAgent:
t('progress.generatingSection', title=section.title, current=section_num, total=total_sections)
)
# 足球比分概率章节使用后端已计算结果避免核心预测被LLM漏写
if self._is_computed_prediction_section(section, section_num):
section_content = football_prediction_to_markdown(self.computed_prediction)
# 足球概率报告使用后端已计算结果避免LLM把用户提示词当作模拟事实分析
if self.computed_prediction:
section_content = self._generate_computed_prediction_section(section, section_num)
if self.report_logger:
self.report_logger.log_section_content(
section_title=section.title,

7
backend/test_health.ps1 Normal file
View File

@ -0,0 +1,7 @@
try {
$r = Invoke-WebRequest -Uri 'http://127.0.0.1:5001/health' -TimeoutSec 3 -ErrorAction Stop
Write-Host "Status: $($r.StatusCode)"
Write-Host "Body: $($r.Content)"
} catch {
Write-Host "Error: $($_.Exception.Message)"
}

View File

@ -994,6 +994,7 @@ dependencies = [
{ name = "charset-normalizer" },
{ name = "flask" },
{ name = "flask-cors" },
{ name = "neo4j" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "pymupdf" },
@ -1022,6 +1023,7 @@ requires-dist = [
{ name = "charset-normalizer", specifier = ">=3.0.0" },
{ name = "flask", specifier = ">=3.0.0" },
{ name = "flask-cors", specifier = ">=6.0.0" },
{ name = "neo4j", specifier = "==5.23.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "pipreqs", marker = "extra == 'dev'", specifier = ">=0.5.0" },
{ name = "pydantic", specifier = ">=2.0.0" },