playlist-admin 2.6.82 → 2.6.84
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.
- package/cli/index.js +7 -1
- package/package.json +3 -2
- package/rag/build_db.py +3 -0
- package/rag/query.py +64 -1
- package/rag/__pycache__/_resolve.cpython-311.pyc +0 -0
- package/rag/__pycache__/build_db.cpython-311.pyc +0 -0
- package/rag/__pycache__/query.cpython-311.pyc +0 -0
- package/rag/__pycache__/srt_to_text.cpython-311.pyc +0 -0
package/cli/index.js
CHANGED
|
@@ -54,10 +54,16 @@ function python() {
|
|
|
54
54
|
function forward(exe, args) {
|
|
55
55
|
// Deliver CLI args via PA_CLI_ARGS (JSON) — Flutter Dart exposes no
|
|
56
56
|
// command-line args in release on this SDK (see lib/main.dart).
|
|
57
|
+
// PA_ROOT lets the Python bridge find rag/ scripts when invoked from the CLI.
|
|
58
|
+
const root = projectRoot();
|
|
57
59
|
const child = spawn(exe, [], {
|
|
58
60
|
stdio: 'inherit',
|
|
59
61
|
windowsHide: false,
|
|
60
|
-
env: {
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
PA_CLI_ARGS: JSON.stringify(args),
|
|
65
|
+
...(root ? { PA_ROOT: root } : {}),
|
|
66
|
+
},
|
|
61
67
|
});
|
|
62
68
|
child.on('exit', (code) => process.exit(code ?? 1));
|
|
63
69
|
child.on('error', (e) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playlist-admin",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.84",
|
|
4
4
|
"description": "Playlist Administrator CLI - thin launcher for the Flutter app engine (pipeline, podcast, spotube) + podcast RAG query",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"cli/index.js",
|
|
20
|
-
"rag
|
|
20
|
+
"rag/*.py",
|
|
21
|
+
"rag/README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"playlist",
|
package/rag/build_db.py
CHANGED
package/rag/query.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
python rag/query.py "問題" --topk 8 # 文字輸出
|
|
4
4
|
python rag/query.py "問題" --json # JSON 輸出 (含 full_text)
|
|
5
5
|
python rag/query.py "問題" --no-full # 不回傳全文, 只回傳命中片段
|
|
6
|
+
python rag/query.py "問題" --answer # 用 Ollama qwen2.5:7b 生成回答
|
|
6
7
|
"""
|
|
7
8
|
import argparse
|
|
8
9
|
import json
|
|
@@ -19,7 +20,11 @@ import requests
|
|
|
19
20
|
from build_db import read_text_file
|
|
20
21
|
from _resolve import chroma_db_dir
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
24
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
25
|
+
|
|
26
|
+
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
|
27
|
+
CHAT_MODEL = os.environ.get("OLLAMA_CHAT_MODEL", "qwen2.5:7b")
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
def embed_one(model: str, text: str) -> list[float]:
|
|
@@ -43,6 +48,57 @@ def locate_chunk(full_norm: str, chunk: str, window: int = 100) -> int:
|
|
|
43
48
|
return idx
|
|
44
49
|
|
|
45
50
|
|
|
51
|
+
def pick_chat_models() -> list[str]:
|
|
52
|
+
"""回傳可嘗試的 chat 模型順序:env 指定 → 本機非 embedding 模型 → cloud 模型"""
|
|
53
|
+
forced = os.environ.get("OLLAMA_CHAT_MODEL", "").strip()
|
|
54
|
+
try:
|
|
55
|
+
tags = requests.get(f"{OLLAMA_URL}/api/tags", timeout=30).json().get("models", [])
|
|
56
|
+
except Exception:
|
|
57
|
+
return [forced] if forced else []
|
|
58
|
+
local = [m["name"] for m in tags if m.get("size", 0) > 0]
|
|
59
|
+
cloud = [m["name"] for m in tags if m.get("size", 0) == 0]
|
|
60
|
+
banned = {"bge-m3", "bge-m3:latest"}
|
|
61
|
+
local = [m for m in local if m not in banned]
|
|
62
|
+
if forced:
|
|
63
|
+
return [forced, *local, *cloud]
|
|
64
|
+
return [*local, *cloud]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def generate_answer(question: str, hits: list[dict]) -> str:
|
|
68
|
+
"""用 Ollama 生成基於檢索片段的回答;全部模型都失敗就回傳錯誤訊息"""
|
|
69
|
+
context = "\n".join(
|
|
70
|
+
f"[{h['show']} | {h['date']} | {h['file']}]\n{h['chunk']}"
|
|
71
|
+
for h in hits
|
|
72
|
+
)
|
|
73
|
+
prompt = (
|
|
74
|
+
"你是 Podcast 內容助理。請只根據以下 Podcast 逐字稿片段回答使用者的問題,"
|
|
75
|
+
"回答用繁體中文,簡潔、分點陳述,並在結尾列出引用來源(節目、日期、檔名)。\n\n"
|
|
76
|
+
f"片段資料:\n{context}\n\n問題:{question}"
|
|
77
|
+
)
|
|
78
|
+
errors = []
|
|
79
|
+
for model in pick_chat_models():
|
|
80
|
+
try:
|
|
81
|
+
resp = requests.post(
|
|
82
|
+
f"{OLLAMA_URL}/api/chat",
|
|
83
|
+
json={
|
|
84
|
+
"model": model,
|
|
85
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
86
|
+
"stream": False,
|
|
87
|
+
},
|
|
88
|
+
timeout=600,
|
|
89
|
+
)
|
|
90
|
+
if resp.status_code in (400, 401, 404):
|
|
91
|
+
errors.append(f"{model}: HTTP {resp.status_code}")
|
|
92
|
+
continue
|
|
93
|
+
resp.raise_for_status()
|
|
94
|
+
return resp.json()["message"]["content"].strip()
|
|
95
|
+
except requests.RequestException as e:
|
|
96
|
+
errors.append(f"{model}: {e}")
|
|
97
|
+
if errors:
|
|
98
|
+
raise RuntimeError(" | ".join(errors))
|
|
99
|
+
return ""
|
|
100
|
+
|
|
101
|
+
|
|
46
102
|
def main() -> None:
|
|
47
103
|
ap = argparse.ArgumentParser()
|
|
48
104
|
ap.add_argument("question", nargs="?", help="要查的問題")
|
|
@@ -53,6 +109,8 @@ def main() -> None:
|
|
|
53
109
|
ap.add_argument("--no-full", action="store_true", help="不回傳全文")
|
|
54
110
|
ap.add_argument("--out", default="", help="JSON 輸出檔路徑 (與 --json 併用)")
|
|
55
111
|
ap.add_argument("--show", default="", help="只搜指定節目")
|
|
112
|
+
ap.add_argument("--answer", action="store_true", help="用 Ollama 生成回答 (qwen2.5:7b)")
|
|
113
|
+
ap.add_argument("--topk-answer", type=int, default=6, help="生成回答使用的片段數")
|
|
56
114
|
args = ap.parse_args()
|
|
57
115
|
|
|
58
116
|
if not args.question:
|
|
@@ -120,6 +178,11 @@ def main() -> None:
|
|
|
120
178
|
|
|
121
179
|
if args.json:
|
|
122
180
|
payload = {"question": args.question, "results": results}
|
|
181
|
+
if args.answer:
|
|
182
|
+
try:
|
|
183
|
+
payload["answer"] = generate_answer(args.question, raw_hits[: max(args.topk_answer, 3)])
|
|
184
|
+
except Exception as e:
|
|
185
|
+
payload["answer_error"] = f"(Ollama 生成失敗: {e})"
|
|
123
186
|
out = json.dumps(payload, ensure_ascii=False, indent=2)
|
|
124
187
|
if args.out:
|
|
125
188
|
Path(args.out).write_text(out, encoding="utf-8")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|