quant-llm-wiki 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quant_llm_wiki/__init__.py +0 -0
- quant_llm_wiki/agent/__init__.py +3 -0
- quant_llm_wiki/agent/cli.py +133 -0
- quant_llm_wiki/agent/graph.py +92 -0
- quant_llm_wiki/agent/prompts.py +48 -0
- quant_llm_wiki/agent/tools.py +714 -0
- quant_llm_wiki/cli.py +29 -0
- quant_llm_wiki/embed.py +359 -0
- quant_llm_wiki/enrich.py +648 -0
- quant_llm_wiki/ingest/__init__.py +0 -0
- quant_llm_wiki/ingest/wechat.py +501 -0
- quant_llm_wiki/query/__init__.py +0 -0
- quant_llm_wiki/query/brainstorm.py +833 -0
- quant_llm_wiki/query/rethink.py +459 -0
- quant_llm_wiki/shared.py +567 -0
- quant_llm_wiki/sync.py +170 -0
- quant_llm_wiki-0.2.0.dist-info/METADATA +544 -0
- quant_llm_wiki-0.2.0.dist-info/RECORD +22 -0
- quant_llm_wiki-0.2.0.dist-info/WHEEL +5 -0
- quant_llm_wiki-0.2.0.dist-info/entry_points.txt +2 -0
- quant_llm_wiki-0.2.0.dist-info/licenses/LICENSE +21 -0
- quant_llm_wiki-0.2.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI entry point for the knowledge base agent.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
# Interactive multi-turn conversation
|
|
6
|
+
python3 agent_cli.py
|
|
7
|
+
|
|
8
|
+
# Single command
|
|
9
|
+
python3 agent_cli.py --query "list all articles"
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from langchain_core.messages import HumanMessage
|
|
19
|
+
|
|
20
|
+
from quant_llm_wiki.shared import _sanitize_lone_surrogates
|
|
21
|
+
|
|
22
|
+
ROOT = Path(__file__).resolve().parent.parent.parent
|
|
23
|
+
if str(ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(ROOT))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _safe_text(value: Any) -> str:
|
|
28
|
+
"""Coerce to str and strip lone surrogates so print/encode never crashes.
|
|
29
|
+
|
|
30
|
+
Why: defense in depth at the output boundary — the agent's `post_model_hook`
|
|
31
|
+
and `post_llm_json` already clean upstream, but nothing stops a stray
|
|
32
|
+
surrogate from arriving here via a tool message, exception repr, etc.
|
|
33
|
+
"""
|
|
34
|
+
text = value if isinstance(value, str) else str(value)
|
|
35
|
+
return _sanitize_lone_surrogates(text)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_args() -> argparse.Namespace:
|
|
39
|
+
parser = argparse.ArgumentParser(description="Knowledge base agent CLI")
|
|
40
|
+
parser.add_argument("--query", help="Single query to run (non-interactive mode)")
|
|
41
|
+
return parser.parse_args()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _extract_last_ai_content(messages) -> str:
|
|
45
|
+
"""Extract the last AI message content that isn't a pure tool call."""
|
|
46
|
+
for msg in reversed(messages):
|
|
47
|
+
if hasattr(msg, "type") and msg.type == "ai" and msg.content and not getattr(msg, "tool_calls", None):
|
|
48
|
+
return msg.content
|
|
49
|
+
if hasattr(msg, "type") and msg.type == "ai" and msg.content:
|
|
50
|
+
return msg.content
|
|
51
|
+
return str(messages[-1].content) if messages else "No response."
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_query(agent, query: str) -> str:
|
|
55
|
+
"""Run a single query through the agent, streaming intermediate output."""
|
|
56
|
+
messages = []
|
|
57
|
+
for state in agent.stream(
|
|
58
|
+
{"messages": [{"role": "user", "content": query}]},
|
|
59
|
+
stream_mode="values",
|
|
60
|
+
):
|
|
61
|
+
messages = state.get("messages", messages)
|
|
62
|
+
return _extract_last_ai_content(messages)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def interactive_loop(agent) -> None:
|
|
66
|
+
"""Run an interactive multi-turn conversation."""
|
|
67
|
+
print("Knowledge Base Agent (type 'quit' or 'exit' to stop)")
|
|
68
|
+
print("-" * 50)
|
|
69
|
+
messages: list = []
|
|
70
|
+
while True:
|
|
71
|
+
try:
|
|
72
|
+
user_input = input("\nYou: ").strip()
|
|
73
|
+
except (EOFError, KeyboardInterrupt):
|
|
74
|
+
print("\nBye!")
|
|
75
|
+
break
|
|
76
|
+
if not user_input:
|
|
77
|
+
continue
|
|
78
|
+
if user_input.lower() in ("quit", "exit", "q"):
|
|
79
|
+
print("Bye!")
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
messages.append(HumanMessage(content=user_input))
|
|
83
|
+
try:
|
|
84
|
+
for state in agent.stream(
|
|
85
|
+
{"messages": messages},
|
|
86
|
+
stream_mode="values",
|
|
87
|
+
):
|
|
88
|
+
messages = state.get("messages", messages)
|
|
89
|
+
print(f"\nAgent: {_safe_text(_extract_last_ai_content(messages))}")
|
|
90
|
+
except KeyboardInterrupt:
|
|
91
|
+
print("\nInterrupted.")
|
|
92
|
+
break
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
print(_safe_text(f"\nError ({type(exc).__name__}): {exc}"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def register(parser: argparse.ArgumentParser) -> None:
|
|
98
|
+
"""Attach this module's CLI flags to `parser`. Called by quant_llm_wiki.cli."""
|
|
99
|
+
parser.add_argument("--query", help="Single query to run (non-interactive mode)")
|
|
100
|
+
parser.set_defaults(func=_run)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _run(args) -> int:
|
|
104
|
+
"""The module's command body. Receives parsed args from the dispatcher."""
|
|
105
|
+
from quant_llm_wiki.agent import create_agent
|
|
106
|
+
|
|
107
|
+
agent = create_agent()
|
|
108
|
+
|
|
109
|
+
if args.query:
|
|
110
|
+
try:
|
|
111
|
+
print(_safe_text(run_query(agent, args.query)))
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
print(
|
|
114
|
+
_safe_text(f"Error ({type(exc).__name__}): {exc}"),
|
|
115
|
+
file=sys.stderr,
|
|
116
|
+
)
|
|
117
|
+
return 1
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
interactive_loop(agent)
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> int:
|
|
125
|
+
"""Standalone entry: python -m quant_llm_wiki.agent.cli ..."""
|
|
126
|
+
parser = argparse.ArgumentParser(description="Knowledge base agent CLI")
|
|
127
|
+
register(parser)
|
|
128
|
+
args = parser.parse_args()
|
|
129
|
+
return args.func(args)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ROOT = Path(__file__).resolve().parent.parent.parent
|
|
7
|
+
if str(ROOT) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(ROOT))
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from langchain_core.messages import AIMessage
|
|
13
|
+
from langchain_openai import ChatOpenAI
|
|
14
|
+
from langgraph.prebuilt import create_react_agent
|
|
15
|
+
|
|
16
|
+
from quant_llm_wiki.agent.prompts import SYSTEM_PROMPT
|
|
17
|
+
from quant_llm_wiki.agent.tools import ALL_TOOLS
|
|
18
|
+
from quant_llm_wiki.shared import _sanitize_response_strings, get_llm_config
|
|
19
|
+
|
|
20
|
+
# AIMessage fields that may carry provider-emitted strings and therefore can
|
|
21
|
+
# transport lone UTF-16 surrogates. We deliberately skip `response_metadata`
|
|
22
|
+
# (model name / finish reason / token counts — low-risk, rarely user-visible)
|
|
23
|
+
# and `usage_metadata` (numeric).
|
|
24
|
+
_SANITIZED_FIELDS = ("content", "tool_calls", "invalid_tool_calls", "additional_kwargs")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _sanitize_agent_message(state: dict[str, Any]) -> dict[str, Any]:
|
|
28
|
+
"""Strip lone UTF-16 surrogates from the most recent AIMessage.
|
|
29
|
+
|
|
30
|
+
Why: ChatOpenAI bypasses our `post_llm_json` sanitizer, so any surrogate
|
|
31
|
+
halves emitted by the provider would reach LangGraph's stream/print path
|
|
32
|
+
and crash on UTF-8 encode. Returning a copy of the same message (same id)
|
|
33
|
+
causes the `add_messages` reducer to replace it in place rather than
|
|
34
|
+
appending a duplicate.
|
|
35
|
+
|
|
36
|
+
Scope: only AIMessage. Tool outputs (ToolMessage) come from our own tool
|
|
37
|
+
code — if a tool ever returned surrogate-bearing text, the output-boundary
|
|
38
|
+
`_safe_text` in `agent/cli.py` catches it on print. Sanitizing ToolMessage
|
|
39
|
+
in the hook would shift framework-internal state for a contingency we
|
|
40
|
+
don't actually have a producer for.
|
|
41
|
+
"""
|
|
42
|
+
messages = state.get("messages") or []
|
|
43
|
+
if not messages:
|
|
44
|
+
return {}
|
|
45
|
+
last = messages[-1]
|
|
46
|
+
if not isinstance(last, AIMessage):
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
update: dict[str, Any] = {}
|
|
50
|
+
for field in _SANITIZED_FIELDS:
|
|
51
|
+
original = getattr(last, field, None)
|
|
52
|
+
if not original:
|
|
53
|
+
continue
|
|
54
|
+
cleaned = _sanitize_response_strings(original)
|
|
55
|
+
if cleaned != original:
|
|
56
|
+
update[field] = cleaned
|
|
57
|
+
|
|
58
|
+
if not update:
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
return {"messages": [last.model_copy(update=update)]}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_agent():
|
|
65
|
+
"""Create and return a compiled LangGraph ReAct agent.
|
|
66
|
+
|
|
67
|
+
Uses the OpenAI-compatible API configured via LLM_* or ZHIPU_* env vars.
|
|
68
|
+
Works with any provider: Zhipu GLM, DeepSeek, Moonshot, Qwen, OpenAI, etc.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
api_key, base_url, model = get_llm_config()
|
|
72
|
+
except RuntimeError as exc:
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
f"Failed to initialize agent LLM: {exc}\n"
|
|
75
|
+
f"Configure via .env file or environment variables. "
|
|
76
|
+
f"See llm_config.example.env for examples."
|
|
77
|
+
) from exc
|
|
78
|
+
|
|
79
|
+
llm = ChatOpenAI(
|
|
80
|
+
model=model,
|
|
81
|
+
api_key=api_key,
|
|
82
|
+
base_url=base_url,
|
|
83
|
+
temperature=0.1,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
agent = create_react_agent(
|
|
87
|
+
model=llm,
|
|
88
|
+
tools=ALL_TOOLS,
|
|
89
|
+
prompt=SYSTEM_PROMPT,
|
|
90
|
+
post_model_hook=_sanitize_agent_message,
|
|
91
|
+
)
|
|
92
|
+
return agent
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
SYSTEM_PROMPT = """你是量化投研知识库管理助手。你管理一个完整的知识库流水线,包括文章抓取、LLM结构化增强、状态审核、向量索引、Wiki概念合成和RAG问答/脑暴。
|
|
2
|
+
|
|
3
|
+
你可以使用以下工具:
|
|
4
|
+
|
|
5
|
+
1. **ingest_article** — 抓取文章并保存到 articles/raw/。支持多种输入:
|
|
6
|
+
- url: 单个URL(自动识别 WeChat / 通用网页 / PDF)
|
|
7
|
+
- urls: 多个URL(换行/逗号分隔)
|
|
8
|
+
- url_list_file: URL 列表文件
|
|
9
|
+
- html_file: 本地 HTML 文件(WeChat 风格)
|
|
10
|
+
- pdf_file: 本地 PDF 文件
|
|
11
|
+
- pdf_url: 远程 PDF URL
|
|
12
|
+
2. **enrich_articles** — 对原始文章进行 LLM 结构化增强(生成 idea_blocks 等字段)
|
|
13
|
+
3. **list_articles** — 列出各阶段文章
|
|
14
|
+
4. **review_articles** — 展示待审核文章
|
|
15
|
+
5. **set_article_status** — 批量更新文章状态
|
|
16
|
+
6. **embed_knowledge** — 构建/更新 ChromaDB 向量索引(同时索引 wiki/)
|
|
17
|
+
7. **query_knowledge_base** — 问答(ask) / 脑暴(brainstorm)。Wiki 概念优先;向量库仅作为补充/兜底
|
|
18
|
+
8. **compile_wiki** — 由文章合成 wiki 概念文章和 source 摘要。模式: incremental(默认)/ rebuild
|
|
19
|
+
9. **list_concepts** — 列出 wiki 概念,按状态筛选(stable / proposed / deprecated)
|
|
20
|
+
10. **set_concept_status** — 批准 / 弃用 / 删除概念(stable / deprecated / deleted)
|
|
21
|
+
11. **read_wiki** — 读取 INDEX、概念文章或 source 摘要
|
|
22
|
+
|
|
23
|
+
## Wiki 层使用指南
|
|
24
|
+
|
|
25
|
+
- **"解释 X" / "梳理 Y" / "总结知识库对 Z 怎么说"** → 优先 read_wiki,target 用概念 slug。如概念不存在,才退回 query_knowledge_base
|
|
26
|
+
- **"脑暴" / "组合想法" / "新策略"** → query_knowledge_base(mode='brainstorm')。它会自动优先检索 wiki 概念,再用复杂检索找互补文章
|
|
27
|
+
- **"找包含 X 的文章" / "做新颖度检查"** → query_knowledge_base(mode='ask')
|
|
28
|
+
|
|
29
|
+
## 典型工作流
|
|
30
|
+
|
|
31
|
+
### 完整入库流程
|
|
32
|
+
ingest_article → enrich_articles → review_articles → set_article_status → compile_wiki → embed_knowledge
|
|
33
|
+
|
|
34
|
+
注意:所有文章统一存放在 raw/ 下,frontmatter 的 status 字段决定其阶段(reviewed / high_value / rejected)。compile_wiki 读取所有非 raw 状态的文章。embed_knowledge 在 compile_wiki 之后运行,使新合成的 wiki 内容也进入向量索引。
|
|
35
|
+
|
|
36
|
+
### 概念审核流程
|
|
37
|
+
当 compile_wiki 报告有 N 个 proposed 概念时:
|
|
38
|
+
1. 调用 list_concepts(status='proposed') 展示
|
|
39
|
+
2. 等待用户决定哪些批准、哪些拒绝
|
|
40
|
+
3. 调用 set_concept_status 批量处理
|
|
41
|
+
4. 如有批准,建议再次运行 compile_wiki 以让批准的概念被纳入合成
|
|
42
|
+
|
|
43
|
+
## 规则
|
|
44
|
+
- 用用户使用的语言回复(中文或英文)
|
|
45
|
+
- 报告结果时清晰简洁,不要编造
|
|
46
|
+
- 链式操作时,每步完成后报告结果再继续下一步
|
|
47
|
+
- 只执行用户明确要求的操作,不要自动链式执行未请求的步骤
|
|
48
|
+
"""
|