Improve LLM and CSV processing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+61
-18
@@ -115,6 +115,9 @@ def scan_regex(text: str, mapping: Dict[str, str], counters: Dict[str, int]) ->
|
||||
class CancelRequested(Exception):
|
||||
"""Обработка прервана пользователем (мягкая остановка между файлами LLM)."""
|
||||
|
||||
def __init__(self, llm_failures: int = 0):
|
||||
self.llm_failures = llm_failures
|
||||
|
||||
# Приоритет границ для чанков (от предпочтительных к жёстким)
|
||||
_CHUNK_BOUNDARIES = ['\n\n', '\n', '. ', '? ', '! ', '; ', ', ', ' ']
|
||||
_CHUNK_SIZE = 6000 # символов на чанк (≈1500-2000 токенов RU — NER-качество)
|
||||
@@ -202,26 +205,53 @@ def _build_llm_prompt(text: str, already_found: List[str]) -> str:
|
||||
|
||||
|
||||
def _call_llm(text: str, llm_client) -> List[dict]:
|
||||
"""Один вызов LLM по фрагменту → список сущностей (пустой при ошибке)."""
|
||||
"""Один вызов LLM по фрагменту → проверенный список сущностей."""
|
||||
raw = llm_client.complete(text).strip()
|
||||
if raw.startswith("```"):
|
||||
lines = raw.splitlines()
|
||||
if lines and lines[0].startswith("```"):
|
||||
lines = lines[1:]
|
||||
if lines and lines[-1].strip() == "```":
|
||||
lines = lines[:-1]
|
||||
raw = "\n".join(lines).strip()
|
||||
|
||||
try:
|
||||
raw = llm_client.complete(text)
|
||||
raw = raw.strip()
|
||||
if raw.startswith("```"):
|
||||
raw = raw.split("\n", 1)[1]
|
||||
if raw.endswith("```"):
|
||||
raw = raw[:-3]
|
||||
entities = json.loads(raw)
|
||||
if not isinstance(entities, list):
|
||||
return []
|
||||
return entities
|
||||
except Exception as e:
|
||||
log.warning("LLM NER failed: %s", e)
|
||||
return []
|
||||
except json.JSONDecodeError as original_error:
|
||||
# Модель иногда добавляет текст вокруг массива; raw_decode корректно
|
||||
# учитывает вложенные массивы и квадратные скобки внутри JSON-строк.
|
||||
decoder = json.JSONDecoder()
|
||||
entities = None
|
||||
for index, char in enumerate(raw):
|
||||
if char != "[":
|
||||
continue
|
||||
try:
|
||||
candidate, _ = decoder.raw_decode(raw, index)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(candidate, list):
|
||||
entities = candidate
|
||||
break
|
||||
if entities is None:
|
||||
raise ValueError("LLM response does not contain a JSON array") from original_error
|
||||
|
||||
if not isinstance(entities, list):
|
||||
raise ValueError("LLM response must be a JSON array")
|
||||
for entity in entities:
|
||||
if not isinstance(entity, dict):
|
||||
raise ValueError("LLM entity must be a JSON object")
|
||||
value = entity.get("value")
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise ValueError("LLM entity value must be a non-empty string")
|
||||
entity_type = entity.get("type")
|
||||
if entity_type is not None and not isinstance(entity_type, str):
|
||||
raise ValueError("LLM entity type must be a string")
|
||||
return entities
|
||||
|
||||
|
||||
def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str],
|
||||
llm_client, counters: Dict[str, int],
|
||||
cancel_event=None, file_progress=None) -> None:
|
||||
cancel_event=None, file_progress=None) -> int:
|
||||
"""Универсальное LLM-обнаружение приватных данных (пофайлово, целиком).
|
||||
|
||||
Каждый файл обрабатывается ПОЛНОСТЬЮ: текст разбивается на чанки
|
||||
@@ -243,11 +273,12 @@ def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str],
|
||||
file_items = sorted(
|
||||
all_texts.items(), key=lambda kv: len(kv[1]), reverse=True
|
||||
)
|
||||
llm_failures = 0
|
||||
|
||||
for fname, full_text in file_items:
|
||||
# Мягкая остановка ТОЛЬКО между файлами — текущий файл добирается до конца
|
||||
if cancel_event is not None and cancel_event.is_set():
|
||||
raise CancelRequested()
|
||||
raise CancelRequested(llm_failures)
|
||||
|
||||
if not full_text or full_text.startswith("[DOC binary"):
|
||||
continue
|
||||
@@ -265,11 +296,19 @@ def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str],
|
||||
for c in chunks]
|
||||
all_ent = []
|
||||
for fut in futures:
|
||||
all_ent.extend(fut.result())
|
||||
try:
|
||||
all_ent.extend(fut.result())
|
||||
except Exception as e:
|
||||
llm_failures += 1
|
||||
log.warning("LLM NER chunk failed in %r: %s", fname, e)
|
||||
else:
|
||||
all_ent = []
|
||||
for k, c in enumerate(chunks):
|
||||
all_ent.extend(_call_llm(_build_llm_prompt(c, mapping.keys()), llm_client))
|
||||
try:
|
||||
all_ent.extend(_call_llm(_build_llm_prompt(c, mapping.keys()), llm_client))
|
||||
except Exception as e:
|
||||
llm_failures += 1
|
||||
log.warning("LLM NER chunk failed in %r: %s", fname, e)
|
||||
if file_progress:
|
||||
file_progress("file_chunk", fname,
|
||||
chunks_done=k + 1, chunks_total=len(chunks))
|
||||
@@ -287,8 +326,12 @@ def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str],
|
||||
if not _verify_entity(val, full_text):
|
||||
log.debug("LLM hallucination dropped: %r", val)
|
||||
continue
|
||||
ent_type = ent.get("type", "").strip().lower().replace(" ", "_")
|
||||
raw_type = ent.get("type")
|
||||
ent_type = raw_type.strip().lower().replace(" ", "_") if raw_type else ""
|
||||
ent_type = ent_type or "other_id"
|
||||
mapping[val] = _next_token(ent_type, counters)
|
||||
|
||||
if file_progress:
|
||||
file_progress("file_done", fname, elapsed=round(time.time() - t0, 2))
|
||||
|
||||
return llm_failures
|
||||
|
||||
Reference in New Issue
Block a user