Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## [Unreleased]

### 新增

- **``main.py`` 支持 ``-h``/``--help``**:旧实现除 ``--smoke-test`` 外静默忽略一切命令行参数,新用户照惯例敲 ``--help`` 直接进交互菜单(实测复现),只能 Ctrl+C 退出重来。现在 ``-h``/``--help`` 在初始化依赖之前打印用法(``--smoke-test``、无参数行为说明)并瞬时退出;回归测试红/绿验证。

### 修复

- **外部模型(codex)全 diff 复核收口 2 处**(九轮 agents 之外的独立视角,逐条探针核实均属实):
Expand Down
31 changes: 29 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
# 导入UI工具
from src.models.document import Document
from src.ui.display_utils import CONSOLE_WIDTH, markup_text
from src.ui.terminal_theme import prompt_style_for_theme

# 从新的配置模块导入 settings 实例
from src.utils.config import get_settings, resolve_app_root
Expand Down Expand Up @@ -371,6 +372,27 @@ def run_cli(argv: list[str] | None = None) -> int:
import 期的失败由 ``get_settings`` 的边界负责(此处尚未执行)。
"""
args = list(sys.argv[1:] if argv is None else argv)
if "-h" in args or "--help" in args:
# 交互式程序被静默吞参数的体验很差:新用户照惯例敲 ``--help`` 时
# 直接进了交互菜单(实测),只能 Ctrl+C 退出重来。给出简明用法
# 后退出,不初始化依赖(``--help`` 必须瞬时返回)。
#
# 同时注销退出清理钩子:``import src.utils.cleanup`` 的模块级
# ``atexit.register`` 早已生效,而本分支从未创建缓存目录——不注销的
# 话 ``--help`` 退出会把**上一次会话**留下的缓存目录整个 rmtree
# (实测复现)。``--help`` 是只读查询,不应有任何副作用。
import atexit

import src.utils.cleanup as _cleanup_module

atexit.unregister(_cleanup_module.cleanup_temp_files)
console.print(markup_text("PyRAG-Kit {}", VERSION))
usage_name = "PyRAG-Kit" if getattr(sys, "frozen", False) else "python main.py"
console.print(markup_text("用法: {} [选项]", usage_name))
console.print(" --smoke-test 非交互式启动自检(构建验证用)")
console.print(" -h, --help 显示本帮助并退出")
console.print(" 无参数 进入交互式菜单(向量化 / 召回测试 / 聊天)")
return 0
try:
if "--smoke-test" in args:
return run_smoke_test()
Expand All @@ -388,8 +410,13 @@ def main():
while True:
display_menu()
try:
# 使用 prompt_toolkit 替代 console.input,并优化样式
choice = prompt(HTML("<skyblue><b>请输入选项 (1-4): </b></skyblue>"))
# 使用 prompt_toolkit 替代 console.input,并优化样式。
# 提示色按终端亮/暗主题适配:固定浅色(xterm 116 skyblue1)在
# 白底上对比度仅 1.7:1(WCAG AA 4.5:1),亮色终端几乎不可读。
choice = prompt(
HTML("<prompt><b>请输入选项 (1-4): </b></prompt>"),
style=prompt_style_for_theme(),
)
if choice == "1":
console.print(
Panel(
Expand Down
4 changes: 2 additions & 2 deletions scripts/audit/regressions/green38.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ src/providers/anthropic.py|684|19|drop|tests/providers/test_guard_coverage_regre
src/providers/anthropic.py|841|71|isnot|tests/providers/test_guard_coverage_regressions.py::test_anthropic_beta_only_fields_rejected_without_beta|KILLED
src/providers/anthropic.py|972|19|isnot||SURVIVED
src/providers/anthropic.py|981|19|isnot|tests/providers/test_guard_coverage_regressions.py::test_anthropic_duplicate_sampling_field_with_extra_body|KILLED
src/ui/config_menu.py|249|15|isnot|tests/ui/test_config_menu.py::test_edit_retrieval_params_writes_rerank_toggle|KILLED
src/ui/config_menu.py|473|15|isnot|tests/ui/test_config_menu.py::test_edit_model_params_writes_selected_rerank_configuration|KILLED
src/ui/config_menu.py|251|15|isnot|tests/ui/test_config_menu.py::test_edit_retrieval_params_writes_rerank_toggle|KILLED
src/ui/config_menu.py|477|15|isnot|tests/ui/test_config_menu.py::test_edit_model_params_writes_selected_rerank_configuration|KILLED
src/retrieval_test/excel_logger.py|346|84|isnot|tests/retrieval_test/test_retrieval_cli.py::test_excel_log_drops_none_scores_from_all_scores_column|KILLED
src/providers/factory.py|356|16|isnot||SURVIVED
src/providers/google.py|1798|27|drop|tests/providers/test_guard_coverage_regressions.py::test_google_tool_result_part_resolves_name_from_call_id|KILLED
Expand Down
5 changes: 3 additions & 2 deletions src/chat/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
get_relative_path,
markup_text,
)
from ..ui.terminal_theme import prompt_style_for_theme
from ..utils.config import get_settings, require_chat_temperature, safe_repr
from ..utils.log_manager import get_chat_logger, get_module_logger
from ..utils.security import safe_exception_text
Expand Down Expand Up @@ -387,7 +388,7 @@ async def start_chat_session_async():
console = Console()
bot = Chatbot(console)
try:
session = PromptSession()
session = PromptSession(style=prompt_style_for_theme())
Comment thread
MisonL marked this conversation as resolved.

if bot.llm_model:
# 启动横幅的回显失败不该以「程序级错误」呈现——``chat_rerank_enabled``
Expand Down Expand Up @@ -426,7 +427,7 @@ async def start_chat_session_async():

while True:
try:
user_query = await session.prompt_async(HTML("<skyblue><b>你: </b></skyblue>"))
user_query = await session.prompt_async(HTML("<prompt><b>你: </b></prompt>"))
if not user_query.strip():
continue
if user_query.lower() == "/quit":
Expand Down
8 changes: 4 additions & 4 deletions src/retrieval_test/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from rich.table import Table
from rich.text import Text

from src.ui.terminal_theme import prompt_style_for_theme

os.environ.setdefault("PROMPT_TOOLKIT_NO_CPR", "1")

from ..retrieval.vdb.factory import VectorStoreFactory
Expand Down Expand Up @@ -84,7 +86,7 @@ async def _aclose_quietly(service: Any) -> None:

async def run_retrieval_test_async():
console.print(Panel("进入异步召回测试模式。输入 '/quit' 退出。", border_style="yellow"))
session = PromptSession()
session = PromptSession(style=prompt_style_for_theme())

excel_logger = None
try:
Expand Down Expand Up @@ -116,9 +118,7 @@ async def run_retrieval_test_async():
try:
while True:
try:
query = await session.prompt_async(
HTML("<deepskyblue><b>测试查询: </b></deepskyblue>")
)
query = await session.prompt_async(HTML("<prompt><b>测试查询: </b></prompt>"))
if query.lower() == "/quit":
break
if not query:
Expand Down
11 changes: 8 additions & 3 deletions src/ui/config_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from ..utils.log_manager import get_module_logger # 导入日志管理器
from .display_utils import _render_value, display_chat_config, markup_text
from .terminal_theme import questionary_style_for_theme

logger = get_module_logger(__name__) # 获取当前模块的日志器
console = Console()
Expand Down Expand Up @@ -180,7 +181,7 @@ def edit_retrieval_params(chat_config: dict[str, Any]) -> None:
questionary.Separator(),
questionary.Choice("返回主菜单", value="back"),
],
style=questionary.Style([("pointer", "bold fg:yellow")]),
style=questionary_style_for_theme(),
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
).ask()

if choice == "back" or choice is None:
Expand All @@ -207,6 +208,7 @@ def edit_retrieval_params(chat_config: dict[str, Any]) -> None:
"选择新的检索模式:",
choices=method_choices,
default=current_method if current_method in method_values else None,
style=questionary_style_for_theme(),
).ask()
if new_method:
chat_config["retrieval_method"] = RetrievalMethod(new_method)
Expand Down Expand Up @@ -314,6 +316,7 @@ def is_float_between_0_and_1(text):
questionary.Choice("Weighted (分值加权)", value="weighted"),
],
default=_normalized_fusion_default(chat_config["hybrid_fusion_strategy"]),
style=questionary_style_for_theme(),
).ask()
if new_strategy:
chat_config["hybrid_fusion_strategy"] = new_strategy
Expand Down Expand Up @@ -453,7 +456,7 @@ def edit_model_params(chat_config: dict[str, Any]) -> bool:
questionary.Separator(),
questionary.Choice("返回主菜单", value="back"),
],
style=questionary.Style([("pointer", "bold fg:yellow")]),
style=questionary_style_for_theme(),
).ask()

if choice == "back" or choice is None:
Expand All @@ -466,6 +469,7 @@ def edit_model_params(chat_config: dict[str, Any]) -> bool:
"选择新的LLM配置:",
choices=llm_options,
default=_choice_default(llm_options, active_llm),
style=questionary_style_for_theme(),
).ask()
# 与**原始**键比较,不与渲染后的 ``current_llm`` 比较:非字符串键
# (或位数超限的整数键)渲染成量级描述后永不等于自身,会让「选中
Expand All @@ -483,6 +487,7 @@ def edit_model_params(chat_config: dict[str, Any]) -> bool:
"选择新的Rerank配置:",
choices=rerank_options,
default=_choice_default(rerank_options, active_rerank),
style=questionary_style_for_theme(),
).ask()
if new_rerank is not None:
chat_config["active_rerank_configuration"] = new_rerank
Expand Down Expand Up @@ -551,7 +556,7 @@ def _run_config_editor_loop(chat_config: dict[str, Any], state: dict[str, bool])
questionary.Separator(),
questionary.Choice("4. 保存并返回聊天", value="exit"),
],
style=questionary.Style([("pointer", "bold fg:cyan")]),
style=questionary_style_for_theme(),
).ask()

if choice == "exit" or choice is None:
Expand Down
91 changes: 91 additions & 0 deletions src/ui/terminal_theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""终端亮/暗主题检测与适配样式。

本项目 TUI 的三处提示色(主菜单 ``prompt``、聊天提示符、questionary 的
pointer)原先写死浅色系(xterm 116 ``skyblue1``、``fg:yellow``)——它们在
暗底上对比度约 12:1,在白底上只有 1.7-2.3:1(WCAG AA 正文要求 4.5:1,实测
低于该阈值),亮色终端下几乎不可读。rich 的语义色不受影响:``Console``
按终端类型自动选择亮/暗变体,问题集中在 prompt_toolkit / questionary 的
固定色。

检测策略(按优先级):

1. 显式环境变量 ``PYRAG_LIGHT=1`` / ``PYRAG_DARK=1``——用户强制指定,
优先于一切自动检测(嵌入式运行或自动检测误判时的逃生通道)。
2. ``COLORFGBG`` 环境变量(``fg;bg`` 或 ``fg;bg;mode`` 格式):背景分量
数值 ≥ 8 视为亮色背景。xterm/rxvt/Konsole 等主流模拟器会设置;
iTerm2 / VS Code 终端默认不设。
3. 兜底暗色:无任何信号时按暗色处理——终端用户以暗色主题为主流,而
亮色主题下错误选择的代价(浅色文字不可读)比暗色主题下更严重。

背景探测只读环境变量,不做 OSC 11 轮询:OSC 查询需要应答式读 tty,
在 ``/config`` 编辑器线程与主循环并发的场景下有竞争风险,不值得为一层
检测精度引入。
"""

from __future__ import annotations

import os

from prompt_toolkit.styles import Style

__all__ = ["detect_light_background", "prompt_style_for_theme", "questionary_style_for_theme"]

# 亮色背景下的替代色(均按白底 ≥4.5:1、黑底 ≥3.8:1 双向校验过):
# 暗色沿用原浅色(黑底 ~12:1),亮色换深变体(白底 ~5:1)。
_PROMPT_COLOR_LIGHT = "#0e7490" # 深青,白底 5.36:1
_PROMPT_COLOR_DARK = "#87d7af" # skyblue1 / xterm 116,黑底 12.37:1
_POINTER_COLOR_LIGHT = "#a16207" # 深黄,白底 4.92:1
_POINTER_COLOR_DARK = "ansiyellow" # 原值,黑底可用


def detect_light_background(environ: dict[str, str] | None = None) -> bool:
"""返回当前终端是否为亮色背景。

``environ`` 供测试注入;默认读 ``os.environ``。
"""
env = os.environ if environ is None else environ
forced_light = env.get("PYRAG_LIGHT", "").strip().lower() in ("1", "true", "yes")
forced_dark = env.get("PYRAG_DARK", "").strip().lower() in ("1", "true", "yes")
if forced_light and not forced_dark:
return True
if forced_dark:
return False

colorfgbg = env.get("COLORFGBG", "")
# 常见形态:"15;0"(fg=15 白 fg / bg=0 黑 bg,暗色终端)、
# "0;15"(暗 fg / 亮 bg,亮色终端)、"0;15;x"(Konsole 三段式)。
parts = [
segment
for segment in colorfgbg.split(";")
if segment and all(character in "0123456789" for character in segment)
]
if len(parts) >= 2:
# bg 永远是**第二段**:两段式 "fg;bg" 自不必说,三段式(Konsole 的
# "fg;bg;mode")里 mode 在 bg 之后——取最后一段会把 mode=0 的亮色
# 终端误判成暗色(实测 "0;15;0" 判 False)。已探针核实。
return int(parts[1]) >= 8
return False


def _prompt_color(light: bool) -> str:
return _PROMPT_COLOR_LIGHT if light else _PROMPT_COLOR_DARK


def prompt_style_for_theme(light: bool | None = None) -> Style:
"""主菜单 / 聊天提示符的 prompt_toolkit 样式。

``light=None`` 时自动检测。原实现把 ``<skyblue>`` 当 HTML class 交给
prompt_toolkit 的 xterm 色名表解析(实际渲染 xterm 116 浅青绿,白底
1.7:1 不可读);现在显式按主题给定 ``prompt`` 类的颜色。
"""
if light is None:
light = detect_light_background()
return Style.from_dict({"prompt": f"bold {_prompt_color(light)}"})


def questionary_style_for_theme(light: bool | None = None) -> Style:
"""questionary 选择菜单的 pointer 样式(两处子菜单共用)。"""
if light is None:
light = detect_light_background()
pointer = _POINTER_COLOR_LIGHT if light else _POINTER_COLOR_DARK
return Style.from_dict({"pointer": f"bold fg:{pointer}"})
17 changes: 15 additions & 2 deletions src/utils/log_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from logging.handlers import RotatingFileHandler

from src.utils.config import get_settings
from src.utils.security import redact_sensitive_text
from src.utils.security import redact_sensitive_text, safe_exception_text

# 日志目录不可写时,降级 warning 只打一次。
#
Expand Down Expand Up @@ -112,7 +112,20 @@ def get_module_logger(name: str) -> logging.Logger:
日志将输出到控制台和文件。
"""
logger = logging.getLogger(name)
current_settings = get_settings()
try:
current_settings = get_settings()
except Exception as exc: # noqa: BLE001 - 日志配置在 import 期不能让进程崩
# 本函数在模块导入期就会被调用(``src/utils/cleanup.py`` 顶层的
# ``get_module_logger(__name__)``),而坏配置会让 ``get_settings``
# 抛 ``ValueError``——用户连 ``--help`` 都看不到(实测:config.toml
# 加一行语法错误后 ``main.py --help`` 以裸 traceback 收场)。日志
# 级别取不到就退回默认值,主流程稍后调用 ``get_settings()`` 时
# 仍会以脱敏的配置错误正常报错。
logger.setLevel(logging.INFO)
if not logger.handlers:
logger.addHandler(logging.StreamHandler())
logger.warning("配置尚未可用,日志按默认级别初始化: %s", safe_exception_text(exc))
return logger
logger.setLevel(current_settings.log_level) # 从配置中获取日志级别

# 避免重复添加处理器
Expand Down
Loading