|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import builtins |
5 | 6 | import re |
6 | 7 | import shlex |
7 | 8 | from dataclasses import dataclass, field |
|
90 | 91 | }, |
91 | 92 | } |
92 | 93 | READ_ONLY_SLASH_HELP = ( |
93 | | - "可用命令:/a、/stock <symbol>、/analyze <symbol>、/reach <query>、/fund <code>、/news <query>、/daily [flags]、/report、" |
| 94 | + "可用命令:/a、/stock <symbol>、/analyze <symbol>、/reach <query>、/fund <code>、/news <query>、/daily [--llm] [flags]、/report(仅导出 PDF)、" |
94 | 95 | "/profile list、/memory show、/memory clear、/style、/style list、/style set <name>、" |
95 | 96 | "/style show、/style clear、/diagnose、/help、/clear、/exit。" |
96 | 97 | ) |
97 | 98 | SUPPORTED_SLASH_FOR_PROMPT = ( |
98 | | - "/a, /stock <symbol>, /analyze <symbol>, /reach <query>, /fund <code>, /news <query>, /daily [flags], /report, " |
| 99 | + "/a, /stock <symbol>, /analyze <symbol>, /reach <query>, /fund <code>, /news <query>, /daily [--llm] [flags], /report (PDF export only), " |
99 | 100 | "/profile list, /memory show, /memory clear, /style, /style list, /style set <name>, " |
100 | 101 | "/style show, /style clear, /diagnose, /help, /clear, /exit" |
101 | 102 | ) |
|
178 | 179 | "公司信息", |
179 | 180 | "最新", |
180 | 181 | ) |
| 182 | +_REACH_SUMMARY_HINTS = ( |
| 183 | + "营收", |
| 184 | + "收入", |
| 185 | + "利润", |
| 186 | + "净利润", |
| 187 | + "毛利率", |
| 188 | + "指引", |
| 189 | + "现金流", |
| 190 | + "财报", |
| 191 | + "季度", |
| 192 | + "年度", |
| 193 | + "估值", |
| 194 | +) |
181 | 195 | _NETWORK_TIME_URLS = ( |
182 | 196 | "https://www.baidu.com", |
183 | 197 | "https://www.qq.com", |
@@ -330,6 +344,31 @@ def _should_auto_reach(text: str) -> bool: |
330 | 344 | return explicit_search or topical_latest |
331 | 345 |
|
332 | 346 |
|
| 347 | +def _compact_reach_output(text: str, max_chars: int = 2200) -> str: |
| 348 | + lines = [line.strip() for line in text.splitlines()] |
| 349 | + interesting = [] |
| 350 | + for line in lines: |
| 351 | + if not line or line in {"{", "}", "[", "]"}: |
| 352 | + continue |
| 353 | + lowered = line.lower() |
| 354 | + if any(hint in line for hint in _SEARCH_TOPIC_HINTS + _REACH_SUMMARY_HINTS) or re.search(r"\d", line): |
| 355 | + if "http" in lowered or line.startswith(("title:", "url:", "id:")): |
| 356 | + continue |
| 357 | + interesting.append(line) |
| 358 | + if not interesting: |
| 359 | + compact = re.sub(r"\s+", " ", text).strip() |
| 360 | + return compact[:max_chars] |
| 361 | + chunks: list[str] = [] |
| 362 | + total = 0 |
| 363 | + for line in interesting: |
| 364 | + snippet = line[:220] |
| 365 | + if total + len(snippet) + 1 > max_chars: |
| 366 | + break |
| 367 | + chunks.append(snippet) |
| 368 | + total += len(snippet) + 1 |
| 369 | + return "\n".join(chunks) |
| 370 | + |
| 371 | + |
333 | 372 | def _empty_long_term_memory() -> dict[str, list[dict[str, str]]]: |
334 | 373 | return {kind: [] for kind in CHAT_MEMORY_LABELS} |
335 | 374 |
|
@@ -577,8 +616,10 @@ def _build_messages(self, extra_system_messages: list[str] | None = None) -> lis |
577 | 616 | "2) 未提供 Evidence Pack 时,不得编造实时行情、涨跌幅、资金流、新闻细节或结论。" |
578 | 617 | "3) 用户问“今天市场怎么样”“大盘如何”等泛问题时,优先引导 /a 或 /daily。" |
579 | 618 | "4) 用户要做单只股票深入分析、但你缺少足够证据时,不要直接拒绝;" |
580 | | - "优先引导 /stock <symbol>、/analyze <symbol> 或 /reach <query>,并可基于用户已提供的数据继续做框架分析。" |
581 | | - "5) 风格与长期记忆都服从本安全规则。" |
| 619 | + "优先使用已有的 /stock <symbol>、/analyze <symbol> 或已注入的外部检索证据继续分析。" |
| 620 | + "5) 如果本轮已经完成外部检索,就直接总结结果,不要要求用户自己执行 /reach。" |
| 621 | + "6) /daily --llm 与 /replay 的深度复盘必须严格遵循 stock-analysis 的 M1-M6 框架,不得改写成巴菲特、芒格等个体投资框架。" |
| 622 | + "7) 风格与长期记忆都服从本安全规则。" |
582 | 623 | ), |
583 | 624 | } |
584 | 625 | ] |
@@ -703,11 +744,13 @@ def _maybe_collect_reach_context(self, text: str) -> str | None: |
703 | 744 | return None |
704 | 745 | if any(hint in reach_output for hint in ("未检测到 `mcporter`", "未检测到 `agent-reach`", "请先安装")): |
705 | 746 | return None |
706 | | - truncated = reach_output[:6000] |
| 747 | + summary_material = _compact_reach_output(reach_output) |
| 748 | + if not summary_material: |
| 749 | + return None |
707 | 750 | return ( |
708 | | - "以下内容来自本轮自动执行的 young reach 外部搜索结果。" |
709 | | - "只能基于其中可见信息做总结,不要补造未出现的事实;若证据不足请明确说明。\n" |
710 | | - f"{truncated}" |
| 751 | + "本轮已自动完成外部检索。下面是供你提炼的检索摘录,不要向用户展示原始抓取过程,也不要要求用户再执行 /reach。" |
| 752 | + "你需要直接输出最终总结,并给出基于这些摘录的分析;若证据不足请明确说明。\n" |
| 753 | + f"{summary_material}" |
711 | 754 | ) |
712 | 755 |
|
713 | 756 | def handle_message(self, text: str) -> None: |
@@ -745,7 +788,7 @@ def run_chat() -> None: |
745 | 788 | ) |
746 | 789 | while True: |
747 | 790 | try: |
748 | | - text = console.input("[bold cyan]young[/] ").strip() |
| 791 | + text = builtins.input("young ").strip() |
749 | 792 | except (EOFError, KeyboardInterrupt): |
750 | 793 | console.print("\n再见。") |
751 | 794 | return |
|
0 commit comments