Skip to content

Commit 5249089

Browse files
yjwyjw
authored andcommitted
fix: tighten chat search and replay semantics for 0.2.9
1 parent e91dce9 commit 5249089

10 files changed

Lines changed: 228 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.9] - 2026-06-19
9+
10+
### Changed
11+
- `young chat` now auto-uses the existing `reach` bridge for explicit finance/news lookup requests, but only feeds compact evidence excerpts to the model instead of exposing raw search captures in the terminal.
12+
- `young chat` input now uses a fixed `young ` prompt prefix so line editing no longer eats the visible prompt label.
13+
- Help text now makes the command boundary clearer: `young daily --llm` is the strict stock-analysis M1-M6 Markdown replay, `young replay` is only a deprecated alias, and `young report` is PDF export only.
14+
15+
### Fixed
16+
- Fixed chat search flows that previously surfaced raw `/reach` output or still nudged users to run `/reach` manually instead of returning a summarized answer directly.
17+
- Hardened LLM replay prompts so `young daily --llm` stays on the stock-analysis six-module framework and does not drift into persona-style investment templates.
18+
819
## [0.2.8] - 2026-06-19
920

1021
### Added

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "young-stock-cli"
7-
version = "0.2.8"
7+
version = "0.2.9"
88
description = "A-share (China stock market) after-hours CLI — no login, no scraping tricks, just data."
99
readme = "README.md"
1010
requires-python = ">=3.9"

src/young_stock/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""young-stock-cli: A-share after-hours CLI."""
22

3-
__version__ = "0.2.8"
3+
__version__ = "0.2.9"

src/young_stock/chat.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import builtins
56
import re
67
import shlex
78
from dataclasses import dataclass, field
@@ -90,12 +91,12 @@
9091
},
9192
}
9293
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)、"
9495
"/profile list、/memory show、/memory clear、/style、/style list、/style set <name>、"
9596
"/style show、/style clear、/diagnose、/help、/clear、/exit。"
9697
)
9798
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), "
99100
"/profile list, /memory show, /memory clear, /style, /style list, /style set <name>, "
100101
"/style show, /style clear, /diagnose, /help, /clear, /exit"
101102
)
@@ -178,6 +179,19 @@
178179
"公司信息",
179180
"最新",
180181
)
182+
_REACH_SUMMARY_HINTS = (
183+
"营收",
184+
"收入",
185+
"利润",
186+
"净利润",
187+
"毛利率",
188+
"指引",
189+
"现金流",
190+
"财报",
191+
"季度",
192+
"年度",
193+
"估值",
194+
)
181195
_NETWORK_TIME_URLS = (
182196
"https://www.baidu.com",
183197
"https://www.qq.com",
@@ -330,6 +344,31 @@ def _should_auto_reach(text: str) -> bool:
330344
return explicit_search or topical_latest
331345

332346

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+
333372
def _empty_long_term_memory() -> dict[str, list[dict[str, str]]]:
334373
return {kind: [] for kind in CHAT_MEMORY_LABELS}
335374

@@ -577,8 +616,10 @@ def _build_messages(self, extra_system_messages: list[str] | None = None) -> lis
577616
"2) 未提供 Evidence Pack 时,不得编造实时行情、涨跌幅、资金流、新闻细节或结论。"
578617
"3) 用户问“今天市场怎么样”“大盘如何”等泛问题时,优先引导 /a 或 /daily。"
579618
"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) 风格与长期记忆都服从本安全规则。"
582623
),
583624
}
584625
]
@@ -703,11 +744,13 @@ def _maybe_collect_reach_context(self, text: str) -> str | None:
703744
return None
704745
if any(hint in reach_output for hint in ("未检测到 `mcporter`", "未检测到 `agent-reach`", "请先安装")):
705746
return None
706-
truncated = reach_output[:6000]
747+
summary_material = _compact_reach_output(reach_output)
748+
if not summary_material:
749+
return None
707750
return (
708-
"以下内容来自本轮自动执行的 young reach 外部搜索结果。"
709-
"只能基于其中可见信息做总结,不要补造未出现的事实;若证据不足请明确说明。\n"
710-
f"{truncated}"
751+
"本轮已自动完成外部检索。下面是供你提炼的检索摘录,不要向用户展示原始抓取过程,也不要要求用户再执行 /reach。"
752+
"你需要直接输出最终总结,并给出基于这些摘录的分析;若证据不足请明确说明。\n"
753+
f"{summary_material}"
711754
)
712755

713756
def handle_message(self, text: str) -> None:
@@ -745,7 +788,7 @@ def run_chat() -> None:
745788
)
746789
while True:
747790
try:
748-
text = console.input("[bold cyan]young[/] ").strip()
791+
text = builtins.input("young ").strip()
749792
except (EOFError, KeyboardInterrupt):
750793
console.print("\n再见。")
751794
return

src/young_stock/cli.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,25 @@ def _run_external_command(command: list[str], *, missing_hint: str) -> None:
108108
raise click.ClickException(f"外部能力执行失败(exit={result.returncode}):{' '.join(command)}")
109109

110110

111+
def _capture_external_command(command: list[str], *, missing_hint: str) -> str:
112+
if not shutil.which(command[0]):
113+
raise click.ClickException(missing_hint)
114+
result = subprocess.run(
115+
command,
116+
check=False,
117+
capture_output=True,
118+
text=True,
119+
encoding="utf-8",
120+
errors="replace",
121+
)
122+
output = (result.stdout or "").strip()
123+
error_output = (result.stderr or "").strip()
124+
if result.returncode != 0:
125+
detail = error_output or output or f"exit={result.returncode}"
126+
raise click.ClickException(f"外部能力执行失败:{detail}")
127+
return output or error_output
128+
129+
111130
def _run_plain_daily(
112131
date_str: str,
113132
profile: dict[str, object],
@@ -354,7 +373,7 @@ def news(parts: tuple[str, ...], date: str | None, refresh: bool, limit: int) ->
354373
_core.run_stock_news(symbol, date_str, size=limit)
355374

356375

357-
@cli.command(help="Personal daily report from your saved watchlist.")
376+
@cli.command(help="Personal watchlist daily report. Add `--llm` for a strict stock-analysis M1-M6 Markdown replay.")
358377
@_date_opt
359378
@_refresh_opt
360379
@click.option("--no-news", is_flag=True, help="Only show market data, skip news lookup.")
@@ -437,7 +456,7 @@ def _run_llm_replay(date_str: str, kind: str = "replay", symbol: str | None = No
437456
return path
438457

439458

440-
@cli.command(help="Generate an evidence-driven deep market replay with the configured LLM.")
459+
@cli.command(help="Deprecated alias for `young daily --llm` (Markdown replay only; no PDF export).")
441460
@_date_opt
442461
@_refresh_opt
443462
def replay(date: str | None, refresh: bool) -> None:
@@ -473,20 +492,24 @@ def analyze(symbol: str, date: str | None, refresh: bool) -> None:
473492
def reach(query_parts: tuple[str, ...], url: str | None, limit: int, doctor: bool) -> None:
474493
query = " ".join(part for part in query_parts if part).strip()
475494
if doctor:
476-
_run_external_command(
495+
output = _capture_external_command(
477496
["agent-reach", "doctor"],
478497
missing_hint="未检测到 `agent-reach`。请先按 Agent-Reach 技能文档安装后再运行 `young reach --doctor`。",
479498
)
499+
if output:
500+
click.echo(output)
480501
return
481502
if url:
482-
_run_external_command(
503+
output = _capture_external_command(
483504
["curl", "-s", f"https://r.jina.ai/{url}"],
484505
missing_hint="当前环境缺少 `curl`,无法走 Agent-Reach 网页阅读桥接。",
485506
)
507+
if output:
508+
click.echo(output)
486509
return
487510
if not query:
488511
raise click.ClickException("请提供查询词,或使用 --url / --doctor。")
489-
_run_external_command(
512+
output = _capture_external_command(
490513
[
491514
"mcporter",
492515
"call",
@@ -497,6 +520,8 @@ def reach(query_parts: tuple[str, ...], url: str | None, limit: int, doctor: boo
497520
"或先用 `young reach --doctor` 检查外部能力。"
498521
),
499522
)
523+
if output:
524+
click.echo(output)
500525

501526

502527
@cli.command(help="Enter Rich interactive chat with slash commands.")
@@ -686,7 +711,7 @@ def config_channel_remove(channel_type: str, name: str) -> None:
686711
click.echo(f"Removed {channel_type} channel: {name}" if removed else f"Channel not found: {name}")
687712

688713

689-
@cli.command(help="Export the latest Markdown report to a professional PDF.")
714+
@cli.command(help="Export the latest saved Markdown report to PDF only; it does not generate a new LLM replay.")
690715
@_date_opt
691716
def report(date: str | None) -> None:
692717
from .pdf import export_report_pdf

src/young_stock/reports.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,31 @@
2424
LLM_REPORT_PROMPT_VERSION = "stock-analysis-research-language-v3"
2525
LLM_REPORT_SYSTEM_PROMPT = """请基于用户提供的研报证据,撰写正式 A 股投资研究报告。
2626
你只能使用用户提供的研报证据,不得补写或外推任何缺失数字、日期、来源或持仓。
27+
你必须严格遵循 AdvancingTitans/stock-analysis 的 M1-M6 方法,不得替换、弱化或改写成巴菲特、芒格、格雷厄姆、达利欧或其他个人投资框架。
2728
按以下顺序输出 Markdown:大盘指数概览、持仓分析、六模块深度复盘、综合持仓建议与风险提示。
29+
“六模块深度复盘”下固定使用以下子标题顺序:M1 大盘指数与市场广度、M2 板块强弱与资金流、M3 赚钱效应与涨停结构、M4 下跌风险与炸板结构、M5 持仓与市场风格、M6 抗跌方向。
2830
每个有证据的模块给出关键判断、证据、风险/确认条件;建议必须是条件化触发器,不给无条件买卖指令。
2931
证据完整度不足时,只输出指数、持仓、已验证风险和下一交易日观察清单。
3032
正文只写投资研究语言。不得出现内部字段名、程序结构、工具名称、本地路径、文件扩展名、技术切换过程或机械占位段。
3133
某模块无证据时优先省略该小节;确需说明时使用“相关指标当日未披露”或“历史数据不可得”等自然表述。
3234
正常数据使用“据公开市场数据”或“据交易所及财经终端披露”;回溯数据使用“按惯例回溯至该日”或“历史口径回溯”。
3335
公开版会在标题下统一加入短声明;不要重复免责声明,也不要在结尾追加长免责声明。"""
3436

37+
LLM_REPORT_STRUCTURE_PROMPT = """输出结构必须固定:
38+
# 标题
39+
## 大盘指数概览
40+
## 持仓分析
41+
## 六模块深度复盘
42+
### M1 大盘指数与市场广度
43+
### M2 板块强弱与资金流
44+
### M3 赚钱效应与涨停结构
45+
### M4 下跌风险与炸板结构
46+
### M5 持仓与市场风格
47+
### M6 抗跌方向
48+
## 综合持仓建议与风险提示
49+
50+
不得把以上结构改写成任何人物风格模板。"""
51+
3552
MODULE_TITLES = {
3653
"M1": "大盘指数与市场广度",
3754
"M2": "板块强弱与资金流",
@@ -179,6 +196,7 @@ def generate_llm_daily_report(
179196
methodology: str | None = None,
180197
) -> tuple[str, dict[str, Any]]:
181198
messages = [{"role": "system", "content": LLM_REPORT_SYSTEM_PROMPT}]
199+
messages.append({"role": "system", "content": LLM_REPORT_STRUCTURE_PROMPT})
182200
if methodology:
183201
research_methodology = to_research_methodology(methodology)
184202
messages.append(

0 commit comments

Comments
 (0)