| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- """Lossless source-local reuse of repeated synthesis text and list entries."""
- from collections import Counter
- from copy import deepcopy
- import json
- import re
- TEXT_REUSE_RULE = '''
- 输入压缩规则:每条来源的text_pool只存本来源的原文字串。字段为{"text_parts":[...]}时,字符串直接读取,{"ref":"t1"}取同条text_pool对应全文,按顺序无缝拼接即原字段。所有部分必须完整读取,不遗漏末尾的条件、否定、例外和版本,不跨来源取字串。字段、原路径和状态各自有效,文字相同不表示资格满足。此编码不改变原文;输出仍用原source_id和JSON协议,不展示内部ref/text_pool。'''
- TEXT_REUSE_MIN_BYTES = 96
- TEXT_REUSE_MAX_FRAGMENTS = 256
- TEXT_REUSE_MAX_PATTERN_CHARS = 32768
- TEXT_REUSE_MIN_NET_BYTES = 1024
- TEXT_REUSE_MIN_NET_PERCENT = 5
- def text_reuse_minimum(payload_bytes):
- """Avoid encoding complexity for byte-level wins that cannot justify it."""
- return max(TEXT_REUSE_MIN_NET_BYTES, (payload_bytes * TEXT_REUSE_MIN_NET_PERCENT + 99) // 100)
- def _bytes(value):
- return len(json.dumps(value, ensure_ascii=False).encode('utf-8'))
- def _text_locations(entry):
- """Only prose fields are encoded; IDs, paths, names and markers stay literal."""
- locations = []
- def visit(value, path, root):
- if isinstance(value, dict):
- for key, child in value.items():
- visit(child, (*path, key), root)
- elif isinstance(value, list):
- for index, child in enumerate(value):
- visit(child, (*path, index), root)
- elif isinstance(value, str) and len(value.encode('utf-8')) >= TEXT_REUSE_MIN_BYTES:
- if root == 'citations' and path[-1] != 'quote':
- return
- if root == 'card' and len(path) > 1 and path[1] == 'name':
- return
- if root == 'conditions' and path[-1] not in ('quote', 'requirement', 'user_evidence', 'question'):
- return
- if root == 'match_basis' and path[-1] not in ('fact_quote', 'explanation'):
- return
- locations.append((path, value))
- for root in ('answer', 'citations', 'card', 'conditions', 'match_basis', 'pending_questions'):
- if root in entry:
- visit(entry[root], (root,), root)
- return locations
- def _assign(entry, path, value):
- target = entry
- for key in path[:-1]:
- target = target[key]
- target[path[-1]] = value
- def expand_text_entry(entry):
- """Restore the exact source-local representation for validation and tests."""
- pool = entry.get('text_pool')
- if not isinstance(pool, dict):
- return deepcopy(entry)
- def expand(value):
- if isinstance(value, dict):
- if set(value) == {'text_parts'}:
- parts = value['text_parts']
- if not isinstance(parts, list):
- raise ValueError('invalid_synthesis_text_parts')
- result = []
- for part in parts:
- if isinstance(part, str):
- result.append(part)
- elif (isinstance(part, dict) and set(part) == {'ref'} and isinstance(part['ref'], str)
- and isinstance(pool.get(part['ref']), str)):
- result.append(pool[part['ref']])
- else:
- raise ValueError('invalid_synthesis_text_reference')
- return ''.join(result)
- return {k: expand(v) for k, v in value.items()}
- if isinstance(value, list):
- return [expand(v) for v in value]
- return value
- return expand({k: v for k, v in entry.items() if k != 'text_pool'})
- def _reuse_entry(entry):
- if not isinstance(entry, dict) or not isinstance(entry.get('source_id'), str) or 'text_pool' in entry:
- return entry
- locations = _text_locations(entry)
- candidates = Counter()
- for _, text in locations:
- sentences = re.split(r'(?<=[。!?;;!?\n])', text)
- candidates.update(s for s in sentences if len(s.encode('utf-8')) >= TEXT_REUSE_MIN_BYTES)
- if text not in sentences:
- candidates[text] += 1
- # Bound indexing work; unselected or unique prose remains inline in full.
- ordered = sorted((s for s, count in candidates.items() if count >= 2),
- key=lambda s: (len(s.encode('utf-8')) * (candidates[s] - 1), len(s)), reverse=True)
- selected, remaining = [], TEXT_REUSE_MAX_PATTERN_CHARS
- for text in ordered:
- if len(text) <= remaining:
- selected.append(text)
- remaining -= len(text)
- if len(selected) >= TEXT_REUSE_MAX_FRAGMENTS:
- break
- if not selected:
- return entry
- for _ in range(3):
- pattern = re.compile('|'.join(re.escape(s) for s in sorted(selected, key=len, reverse=True)))
- uses = Counter(m.group() for _, text in locations for m in pattern.finditer(text))
- kept = [s for s in selected if uses[s] >= 2]
- if kept == selected:
- break
- selected = kept
- if not selected:
- return entry
- pattern = re.compile('|'.join(re.escape(s) for s in sorted(selected, key=len, reverse=True)))
- ids = {text: f't{index}' for index, text in enumerate(selected)}
- result = deepcopy(entry)
- count = 0
- for path, text in locations:
- parts, start = [], 0
- for match in pattern.finditer(text):
- if match.group() not in ids:
- continue
- if match.start() > start:
- parts.append(text[start:match.start()])
- parts.append({'ref': ids[match.group()]})
- start = match.end()
- count += 1
- if parts:
- if start < len(text):
- parts.append(text[start:])
- _assign(result, path, {'text_parts': parts})
- if not count:
- return entry
- result['text_pool'] = {identity: text for text, identity in ids.items()}
- # No semantic selection or truncation: every field, path and condition must
- # reconstruct exactly. Skip any representation whose JSON overhead grows.
- try:
- valid = expand_text_entry(result) == entry
- except (ValueError, TypeError, KeyError):
- valid = False
- return result if valid and _bytes(result) < _bytes(entry) else entry
- def text_reuse_prompt(prompt, payload):
- entries = payload.get('evidence', payload.get('cards', []))
- return prompt + TEXT_REUSE_RULE if any(isinstance(e, dict) and 'text_pool' in e for e in entries) else prompt
- def final_model_budget(model, prompt, payload, diagnostics):
- """Choose a call-local output limit; never mutate shared models or evidence."""
- maximum = getattr(model, 'MAX_OUTPUT_TOKENS', None)
- minimum = getattr(model, 'MIN_OUTPUT_TOKENS', None)
- config = getattr(model, 'config', None)
- context = getattr(config, 'context_tokens', None)
- margin = getattr(config, 'margin', None)
- if not all(type(v) is int for v in (maximum, minimum, context, margin)):
- return model
- margin = max(904, margin)
- size = len((prompt + json.dumps(payload, ensure_ascii=False)).encode('utf-8'))
- available = context - margin - size
- # Below the floor, keep the normal budget and let hierarchical reduction run.
- reserve = min(maximum, available) if available >= minimum else maximum
- diagnostics.update(configured_output_token_reserve=maximum, min_output_token_reserve=minimum,
- output_token_reserve=reserve, output_reserve_reduction=maximum - reserve,
- final_call_input_bytes=size, context_limit=context, margin_reserve=margin,
- reserved_total=size + reserve + margin)
- return model.with_output_tokens(reserve) if reserve != maximum else model
- def compact_payload(payload, diagnostics=None):
- result = deepcopy(payload)
- removed = 0
- def unique(rows):
- nonlocal removed
- seen, kept = set(), []
- for row in rows:
- key = json.dumps(row, ensure_ascii=False, sort_keys=True)
- if key in seen:
- removed += 1
- else:
- seen.add(key)
- kept.append(row)
- return kept
- # Do not merge sources, rewrite prose, or normalize quotes and qualifiers.
- for entry in result.get('evidence', result.get('cards', [])):
- for field in ('citations', 'conditions', 'match_basis'):
- if isinstance(entry.get(field), list):
- entry[field] = unique(entry[field])
- if isinstance(result.get('known_subject_facts'), list):
- result['known_subject_facts'] = unique(result['known_subject_facts'])
- key = 'evidence' if 'evidence' in result else 'cards' if 'cards' in result else None
- if key and isinstance(result[key], list):
- packed = {**result, key: [_reuse_entry(entry) for entry in result[key]]}
- savings = _bytes(result) - _bytes(packed)
- already_packed = any(isinstance(e, dict) and 'text_pool' in e for e in result[key])
- net = savings - (0 if already_packed else len(TEXT_REUSE_RULE.encode('utf-8')))
- minimum = text_reuse_minimum(_bytes(result))
- if savings > 0 and diagnostics is not None:
- diagnostics['text_reuse_candidate_net_bytes'] = max(0, net)
- diagnostics['text_reuse_minimum_net_bytes'] = minimum
- diagnostics['text_reuse_applied'] = int(net >= minimum)
- if net >= minimum:
- if diagnostics is not None:
- diagnostics['reused_text_bytes_saved'] = savings
- diagnostics['pooled_texts'] = sum(len(e.get('text_pool', {})) for e in packed[key] if isinstance(e, dict))
- result = packed
- return result, removed
|