Skip to content

Commit 2d0b854

Browse files
author
farfarfun
committed
Update LLM timeout, instructions, handling, test names, and assertions
1 parent 9608a89 commit 2d0b854

4 files changed

Lines changed: 33 additions & 122 deletions

File tree

example/run.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ def add_url_data():
4545
# add_url_data()
4646

4747
# GenerateSourceTask().run_book(sync=True)
48-
GenerateSourceTask().run_book(merge=True)
48+
# GenerateSourceTask().run_book(merge=True)
49+
# GenerateSourceTask().run_book(dump=True)
50+
4951
# GenerateSourceTask().run_rss()
52+
# GenerateSourceTask().run_rss(load=True)
53+
GenerateSourceTask().run_rss(merge=True)
54+
GenerateSourceTask().run_rss(dump=True)
5055
# UpdateEntrance().run()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "funread"
3-
version = "1.1.89"
3+
version = "1.1.90"
44
description = "一个用于管理和处理阅读源(Legado 阅读 APP 的书源和 RSS 源)的 Python 工具库"
55
readme = "README.md"
66
requires-python = ">=3.8"

src/funread/legado/manage/source/merge/task.py

Lines changed: 20 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
DEFAULT_LLM_BASE_URL = "https://api.openai.com/v1"
2424
DEFAULT_LLM_MODEL = "gpt-4.1-mini"
25-
DEFAULT_LLM_TIMEOUT = 600
25+
DEFAULT_LLM_TIMEOUT = 1200
2626
DEFAULT_MAX_VERSIONS_PER_MERGE = 8
2727
DEFAULT_MAX_PROMPT_CHARS = 50000
2828
DEFAULT_LLM_MAX_RETRIES = 3
@@ -88,6 +88,7 @@ def _build_prompt(source_type: str, hostname: str, versions: List[Dict[str, Any]
8888
"请把同一个站点的多个版本阅读源合并成一个最优版本。"
8989
"输出必须是合并后的单个源对象本身,必须是紧凑 JSON。"
9090
"不要输出解释,不要输出 markdown,不要输出数组,不要输出外层包装对象。"
91+
"请直接给出最终答案,不要解释、不要分步骤、不要用‘思考’标签。"
9192
"要求:\n"
9293
"1. 保留同一含义下信息更完整、更具体的字段。\n"
9394
"2. 不要引入原始数据中不存在的新站点 URL。\n"
@@ -98,32 +99,6 @@ def _build_prompt(source_type: str, hostname: str, versions: List[Dict[str, Any]
9899
f"原始版本如下:\n{versions_json}"
99100
)
100101

101-
@staticmethod
102-
def _extract_stream_delta(payload: Dict[str, Any]) -> Dict[str, str]:
103-
choices = payload.get("choices")
104-
if not isinstance(choices, list) or not choices:
105-
return {}
106-
choice = choices[0]
107-
if not isinstance(choice, dict):
108-
return {}
109-
delta = choice.get("delta")
110-
if isinstance(delta, dict):
111-
reasoning = delta.get("reasoning_content")
112-
content = delta.get("content")
113-
result: Dict[str, str] = {}
114-
if isinstance(reasoning, str):
115-
result["reasoning"] = reasoning
116-
if isinstance(content, str):
117-
result["content"] = content
118-
if result:
119-
return result
120-
message = choice.get("message")
121-
if isinstance(message, dict):
122-
content = message.get("content")
123-
if isinstance(content, str):
124-
return {"content": content}
125-
return {}
126-
127102
def _post_and_collect_content(self, payload: Dict[str, Any]) -> str:
128103
headers = {
129104
"Authorization": f"Bearer {self.api_key}",
@@ -135,97 +110,38 @@ def _post_and_collect_content(self, payload: Dict[str, Any]) -> str:
135110
"Start LLM merge request: "
136111
f"model={self.model}, versions_payload_chars={len(json.dumps(payload, ensure_ascii=False))}"
137112
)
138-
with requests.post(
139-
url,
140-
headers=headers,
141-
json=payload,
142-
timeout=self.timeout,
143-
stream=bool(payload.get("stream")),
144-
) as response:
113+
with requests.post(url, headers=headers, json=payload, timeout=self.timeout) as response:
145114
logger.info(
146115
"LLM merge response headers received: "
147116
f"status={response.status_code}, elapsed={time.time() - request_started_at:.2f}s"
148117
)
149118
response.raise_for_status()
150-
if not payload.get("stream"):
151-
try:
152-
result = response.json()
153-
except Exception:
154-
return response.text
155-
choices = result.get("choices", [])
156-
if choices and isinstance(choices[0], dict):
157-
message = choices[0].get("message", {})
158-
if isinstance(message, dict):
159-
content = message.get("content")
160-
if isinstance(content, str):
161-
logger.info(
162-
"LLM merge request finished: "
163-
f"elapsed={time.time() - request_started_at:.2f}s, content_chars={len(content)}"
164-
)
165-
return content
119+
try:
120+
result = response.json()
121+
except Exception:
166122
text = response.text
167123
logger.info(
168124
"LLM merge request finished with raw text: "
169125
f"elapsed={time.time() - request_started_at:.2f}s, content_chars={len(text)}"
170126
)
171127
return text
172-
173-
content_fragments: List[str] = []
174-
reasoning_chars = 0
175-
event_count = 0
176-
first_chunk_logged = False
177-
for raw_line in response.iter_lines(decode_unicode=False):
178-
if not raw_line:
179-
continue
180-
if isinstance(raw_line, bytes):
181-
try:
182-
line = raw_line.decode("utf-8").strip()
183-
except UnicodeDecodeError:
184-
line = raw_line.decode("utf-8", errors="ignore").strip()
185-
logger.warning(
186-
"Decode LLM stream line with ignored utf-8 errors: "
187-
f"raw_bytes={len(raw_line)}"
188-
)
189-
else:
190-
line = raw_line.strip()
191-
if not line.startswith("data:"):
192-
continue
193-
data = line[5:].strip()
194-
if data == "[DONE]":
195-
break
196-
event_count += 1
197-
try:
198-
event_payload = json.loads(data)
199-
except json.JSONDecodeError:
200-
logger.warning(f"Invalid LLM stream payload: {data[:200]}")
201-
continue
202-
delta = self._extract_stream_delta(event_payload)
203-
reasoning_piece = delta.get("reasoning", "")
204-
content_piece = delta.get("content", "")
205-
if reasoning_piece:
206-
reasoning_chars += len(reasoning_piece)
207-
if content_piece:
208-
content_fragments.append(content_piece)
209-
if not first_chunk_logged:
210-
logger.info(
211-
"LLM merge first stream chunk received: "
212-
f"elapsed={time.time() - request_started_at:.2f}s, chunk_chars={len(content_piece)}"
213-
)
214-
first_chunk_logged = True
215-
elif event_count % 20 == 0:
128+
choices = result.get("choices", [])
129+
if choices and isinstance(choices[0], dict):
130+
message = choices[0].get("message", {})
131+
if isinstance(message, dict):
132+
content = message.get("content")
133+
if isinstance(content, str):
216134
logger.info(
217-
"LLM merge stream progress: "
218-
f"elapsed={time.time() - request_started_at:.2f}s, "
219-
f"events={event_count}, reasoning_chars={reasoning_chars}, "
220-
f"content_chars={sum(len(item) for item in content_fragments)}"
135+
"LLM merge request finished: "
136+
f"elapsed={time.time() - request_started_at:.2f}s, content_chars={len(content)}"
221137
)
222-
content = "".join(content_fragments)
138+
return content
139+
text = response.text
223140
logger.info(
224-
"LLM merge stream finished: "
225-
f"elapsed={time.time() - request_started_at:.2f}s, "
226-
f"events={event_count}, reasoning_chars={reasoning_chars}, content_chars={len(content)}"
141+
"LLM merge request finished with raw text: "
142+
f"elapsed={time.time() - request_started_at:.2f}s, content_chars={len(text)}"
227143
)
228-
return content
144+
return text
229145

230146
def merge_sources(
231147
self,
@@ -238,7 +154,6 @@ def merge_sources(
238154
payload = {
239155
"model": self.model,
240156
"temperature": 0,
241-
"stream": True,
242157
"messages": [
243158
{"role": "system", "content": "你只返回一个合法、紧凑、无包装层的 JSON 对象。"},
244159
{
@@ -325,7 +240,7 @@ def iter_source_files(self) -> List[str]:
325240
if name.endswith(".json"):
326241
file_path = os.path.join(root, name)
327242
file_list.append((self._read_version_count(file_path), file_path))
328-
file_list.sort(key=lambda item: (-item[0], item[1]))
243+
file_list.sort(key=lambda item: (item[0], item[1]))
329244
return [file_path for _, file_path in file_list]
330245

331246
def merge_file(self, file_path: str) -> str:

tests/test_download_base.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -528,10 +528,10 @@ def delete(self, fid):
528528
assert context.drive.deleted == ["a/1002", "a/1003"]
529529

530530

531-
def test_openai_compatible_merger_reads_stream_response(monkeypatch) -> None:
531+
def test_openai_compatible_merger_reads_json_response(monkeypatch) -> None:
532532
class _Response:
533533
status_code = 200
534-
text = ""
534+
text = '{"choices":[{"message":{"content":"{\\"ok\\": true}"}}]}'
535535

536536
def __enter__(self):
537537
return self
@@ -542,18 +542,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
542542
def raise_for_status(self):
543543
return None
544544

545-
def iter_lines(self, decode_unicode=True):
546-
return iter(
547-
[
548-
'data: {"choices":[{"delta":{"reasoning_content":"thinking"}}]}',
549-
'data: {"choices":[{"delta":{"content":"{\\"ok\\":"}}]}',
550-
'data: {"choices":[{"delta":{"content":" true}"}}]}',
551-
"data: [DONE]",
552-
]
553-
)
545+
def json(self):
546+
return {"choices": [{"message": {"content": '{"ok": true}'}}]}
554547

555548
def _fake_post(*args, **kwargs):
556-
assert kwargs["stream"] is True
557549
return _Response()
558550

559551
monkeypatch.setattr(merge_module.requests, "post", _fake_post)
@@ -567,7 +559,6 @@ def _fake_post(*args, **kwargs):
567559
content = merger._post_and_collect_content(
568560
{
569561
"model": "deepseek/deepseek-reasoner",
570-
"stream": True,
571562
"messages": [{"role": "user", "content": "x"}],
572563
}
573564
)
@@ -738,7 +729,7 @@ def merge_sources(self, source_type, hostname, versions):
738729
assert data == original
739730

740731

741-
def test_source_merge_runner_prioritizes_more_versions_first(tmp_path: Path) -> None:
732+
def test_source_merge_runner_prioritizes_fewer_versions_first(tmp_path: Path) -> None:
742733
store = BookSourceProcessor(path=str(tmp_path), cate1="book")
743734
source_dir = Path(store.path_bok) / "10000000-10000100"
744735
source_dir.mkdir(parents=True, exist_ok=True)
@@ -787,7 +778,7 @@ def merge_sources(self, source_type, hostname, versions):
787778

788779
ordered = runner.iter_source_files()
789780

790-
assert ordered == [str(large_path), str(small_path)]
781+
assert ordered == [str(small_path), str(large_path)]
791782

792783

793784
def test_source_merge_runner_splits_large_merge_requests(tmp_path: Path) -> None:

0 commit comments

Comments
 (0)