-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraph.py
More file actions
293 lines (216 loc) · 9.82 KB
/
Copy pathgraph.py
File metadata and controls
293 lines (216 loc) · 9.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
import asyncio
from typing import List, Dict
from langchain_core.runnables import Runnable
from langgraph.graph import StateGraph, END
import spacy, json, time, logging, re
from utils.LLMClientManager import LLMclientManager
from utils.Get_term import (
translate_batch_async, translate_term_async,
)
from utils.workflow_adapter import _unwrap
from utils.TimeNode import timed_node
import typing
from utils.TermState import TermState
from Nodes._reflect_node import reflect_sync_node,route_after_reflect
from Nodes._remove_node import remove_sync_node
from Nodes.select_top_terms import select_top_terms,select_top_terms_FAST
from Nodes._terms_only_batch import _terms_only_batch
# ===================== 2️⃣ 初始化 =====================
nlp = spacy.load("en_core_web_trf")
# logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ===================== 9️⃣ 构建 LangGraph 工作流(重构为 main.extract 的批处理流程) =====================
_TERMS_ONLY_WORKFLOW = None
@timed_node()
def _init_extract_state(state: TermState) -> TermState:
original: TermState | tuple | dict = state
inner, parent, key = _unwrap(state)
sd: TermState = typing.cast(TermState, inner if isinstance(inner, dict) else TermState())
# 仅初始化缺失的键,避免重复写入
updates: TermState = {}
if "summary" not in sd:
updates["summary"] = sd.get("summary", "") or ""
return typing.cast(TermState, updates)
@timed_node()
def _aggregate_unique_terms(state: TermState) -> TermState:
inner, parent, key = _unwrap(state)
sd: TermState = typing.cast(TermState, inner if isinstance(inner, dict) else TermState())
# --- 修改开始 ---
# 不要读取 sd["selected_terms"],因为它可能被并行覆盖了
# 从结构化的 chunk_terms 中提取所有出现过的术语
chunk_terms_data = sd.get("chunk_terms", [])
all_extracted_terms = []
if chunk_terms_data:
for item in chunk_terms_data:
# item 是 {'chunk_id': '...', 'terms': [...]}
terms = item.get("terms", [])
if isinstance(terms, list):
all_extracted_terms.extend(terms)
# 去重
unique_terms = sorted(set(t for t in all_extracted_terms if isinstance(t, str) and t.strip()))
# --- 修改结束 ---
logger.info("Aggregated %d unique terms from chunks for translation.", len(unique_terms))
return typing.cast(TermState, {"unique_terms": unique_terms})
@timed_node()
async def _single_translate_concurrent(state: TermState) -> TermState:
"""
翻译节点(极致性能版)
"""
inner, parent, key = _unwrap(state)
sd: TermState = typing.cast(TermState, inner if isinstance(inner, dict) else TermState())
unique_terms: List[str] = sd.get("unique_terms", [])
topic = sd.get("summary", "")
translations_map: Dict[str, List[str]] = {}
if not unique_terms:
return typing.cast(TermState, {"translations_map": translations_map})
# 检查是否有 MT 模型
target_mt_model = "tencent/Hunyuan-MT-7B"
has_mt_model = LLMclientManager.check_model_exists(target_mt_model)
if has_mt_model:
# =====================================================
# 策略 A: 单词高并发 (MT 模型)
# =====================================================
logger.info(f"🚀 启用 MT 高并发模式 ({target_mt_model})")
# 信号量:控制同时飞在天上的请求数,防止 API 限流
# 建议根据你的 API 额度调整,100 是个激进但高效的值
semaphore = asyncio.Semaphore(100)
async def worker(term):
async with semaphore:
# 失败自动重试 2 次
for _ in range(2):
res = await translate_term_async(term, topic, target_mt_model)
if res: return term, res
# 稍微退避一下
# await asyncio.sleep(0.1)
return term, []
# 创建任务并发执行
tasks = [worker(t) for t in unique_terms]
results = await asyncio.gather(*tasks)
for term, res in results:
translations_map[term] = res
else:
# =====================================================
# 策略 B: 批量分块 (通用模型)
# =====================================================
logger.info("📦 启用通用模型批量模式")
batch_size = 20 # 通用模型一次处理 20 个词比较稳
max_concurrency = 10 # 控制并发数
semaphore = asyncio.Semaphore(max_concurrency)
# 切分列表
chunks = [unique_terms[i:i + batch_size] for i in range(0, len(unique_terms), batch_size)]
async def worker_batch(chunk):
async with semaphore:
for _ in range(2): # 简单重试
res = await translate_batch_async(chunk, topic)
if res: return res
return {}
tasks = [worker_batch(c) for c in chunks]
results = await asyncio.gather(*tasks)
for batch_map in results:
if batch_map:
translations_map.update(batch_map)
# 兜底检查
missing = 0
for t in unique_terms:
if t not in translations_map:
translations_map[t] = []
missing += 1
logger.info(f"翻译完成。总数: {len(translations_map)}, 补全空缺: {missing}")
return typing.cast(TermState, {"translations_map": translations_map})
def _assemble_annotations(state: TermState) -> TermState:
import re
original = state
inner, parent, key = _unwrap(state)
sd: TermState = typing.cast(TermState, inner if isinstance(inner, dict) else TermState())
per_chunk_results = sd.get("chunk_terms", [])
translations_map: Dict[str, List[str]] = sd.get("translations_map", {})
term_annotations: Dict[str, typing.Any] = {}
def lookup_candidates(t: str):
key_raw = t
key_lower = t.lower().strip()
return (
translations_map.get(key_raw) or
translations_map.get(key_lower) or
[]
)
# --- 修改点 1:_pick_best 逻辑修正 ---
def _pick_best(term: str, cands: List[str]) -> typing.Optional[str]:
# 只有当完全没有候选词时,才视为“失败”,返回 None
if not cands:
return None
filtered = [c.strip() for c in cands if c and c.strip()]
if not filtered:
return None
term_norm = term.strip().lower()
# 优先策略不变:先找中文
for c in filtered:
if re.search(r"[\u4e00-\u9fff]", c):
return c
# 其次:找和原文不一样的(比如全称扩展)
for c in filtered:
if c.lower() != term_norm:
return c
# 【关键修正】:如果只剩下和原文一样的词(例如 AVL -> AVL),直接返回它
# 只要翻译表里有它,就说明它是有效结果
return filtered[0]
for chunk_item in per_chunk_results:
cid = chunk_item.get("chunk_id")
terms = chunk_item.get("terms", [])
items = []
for t in terms:
cands = lookup_candidates(t)
# 这里的 cands 如果是 [],_pick_best 会返回 None
chosen = _pick_best(t, cands)
# --- 修改点 2:只过滤 None ---
if chosen is None:
# 说明翻译表里根本没这个词(或者值是空的),跳过
continue
items.append({"term": t, "translation": chosen})
# ---------------------------
term_annotations[str(cid)] = items
print(f"Assembled term_annotations: {term_annotations}")
return typing.cast(TermState, {"term_annotations": term_annotations})
@timed_node()
def build_graph() -> Runnable:
graph: StateGraph[TermState] = StateGraph(TermState)
graph.add_node("init", _init_extract_state)
graph.add_node("terms_only_batch", _terms_only_batch)
graph.add_node("select_top_terms", select_top_terms)
graph.add_node("reflect_terms", reflect_sync_node)
graph.add_node("aggregate_unique_terms", _aggregate_unique_terms)
graph.add_node("batch_translate", _single_translate_concurrent)
graph.add_node("assemble_annotations", _assemble_annotations)
graph.set_entry_point("init")
graph.add_edge("init", "terms_only_batch")
graph.add_edge("terms_only_batch", "select_top_terms")
graph.add_edge("select_top_terms", "reflect_terms")
graph.add_conditional_edges(
"reflect_terms",
route_after_reflect,
{
"retry": "select_top_terms", # 仍然只回到 select_top_terms
"proceed": "aggregate_unique_terms",
},
)
graph.add_edge("aggregate_unique_terms", "batch_translate")
graph.add_edge("batch_translate", "assemble_annotations")
graph.add_edge("assemble_annotations", END)
return graph.compile()
@timed_node()
def build_graph_fast() -> Runnable:
graph: StateGraph[TermState] = StateGraph(TermState)
graph.add_node("init", _init_extract_state)
graph.add_node("terms_only_batch", _terms_only_batch)
graph.add_node("select_top_terms", select_top_terms_FAST)
graph.add_node("aggregate_unique_terms", _aggregate_unique_terms)
graph.add_node("batch_translate", _single_translate_concurrent)
graph.add_node("assemble_annotations", _assemble_annotations)
graph.set_entry_point("init")
graph.add_edge("init", "terms_only_batch")
graph.add_edge("terms_only_batch", "select_top_terms")
graph.add_edge("select_top_terms","aggregate_unique_terms")
graph.add_edge("aggregate_unique_terms", "batch_translate")
graph.add_edge("batch_translate", "assemble_annotations")
graph.add_edge("assemble_annotations", END)
return graph.compile()