77 lines
4.4 KiB
Python
77 lines
4.4 KiB
Python
"""Two-stage contracts reject ambiguous grouping and ungrounded conclusions."""
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def pipeline(trading):
|
|
from alphapulse.news_research import pipeline
|
|
return pipeline
|
|
|
|
|
|
def test_evidence_cannot_cross_article_boundaries(pipeline):
|
|
articles = {1: {'title': '央行维持利率'}, 2: {'title': '交易所暂停提现'}}
|
|
with pytest.raises(ValueError, match='原文引用'):
|
|
pipeline.validate_evidence([pipeline.Evidence(article_id=1, quote='交易所暂停提现')], articles)
|
|
pipeline.validate_evidence([pipeline.Evidence(article_id=2, quote='暂停提现')], articles)
|
|
|
|
|
|
def event_result(pipeline, refs):
|
|
return pipeline.ReviewResult(id=1, title='Synthetic event', summary='Synthetic summary', category='macro', entities=[],
|
|
risk_stars=1, importance_stars=4, direction='unknown', verification='insufficient', confidence='low', reasoning='Synthetic', uncertainty='Synthetic',
|
|
timeline_points=[dict(occurred_at=None, time_precision='unknown', time_text='Unknown', reported_at=None, description='Synthetic', verification='insufficient', evidence_refs=[pipeline.Evidence.model_validate(r).model_dump() for r in refs])], facts='Synthetic', timeline='Synthetic', asset_impacts='Unknown', opinions='Unknown', counter_evidence='Unknown', watchpoints='Unknown', claims=[pipeline.Claim(statement='Synthetic', verdict='insufficient', evidence_refs=[])], excluded_article_ids=[], evidence_refs=refs)
|
|
|
|
|
|
@pytest.mark.parametrize('invalid', ['missing_source', 'rewritten_quote', 'no_progress'])
|
|
def test_compaction_rejects_lost_provenance_and_stops_without_progress(pipeline, monkeypatch, invalid):
|
|
provider = SimpleNamespace(max_tokens=100, context_window=8000)
|
|
extracts = [{'facts': 'x'*3000, 'evidence_refs': [{'article_id': i, 'quote': 'original'}]} for i in (1, 2)]
|
|
calls = []
|
|
|
|
async def complete(provider, system, payload, schema):
|
|
calls.append(payload)
|
|
refs = [r for e in payload['extractions'] for r in e['evidence_refs']]
|
|
if invalid == 'missing_source': refs = refs[:1]
|
|
if invalid == 'rewritten_quote': refs[0] = {**refs[0], 'quote': 'invented'}
|
|
facts = 'x'*10000 if invalid == 'no_progress' else 'compressed'
|
|
return pipeline.Extraction(facts=facts, evidence_refs=refs), {}
|
|
|
|
monkeypatch.setattr(pipeline, 'complete', complete)
|
|
message = '未缩减输入' if invalid == 'no_progress' else '遗漏来源或改写'
|
|
with pytest.raises(ValueError, match=message):
|
|
asyncio.run(pipeline.compact_extractions(provider, 'rules', {'task': 'event', 'id': 1, 'extractions': extracts}, pipeline.ReviewResult))
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_extraction_requires_original_evidence(pipeline, monkeypatch):
|
|
async def complete(provider, system, payload, schema):
|
|
return schema.model_validate({'facts': '', 'evidence_refs': []}), {}
|
|
monkeypatch.setattr(pipeline, 'complete', complete)
|
|
with pytest.raises(ValueError):
|
|
asyncio.run(pipeline.extract_articles(SimpleNamespace(max_tokens=100, context_window=12000), 'rules',
|
|
[{'id': 1, 'title': 'Original', 'content': '', 'summary': ''}]))
|
|
with pytest.raises(ValueError):
|
|
event_result(pipeline, [])
|
|
|
|
|
|
def test_long_extraction_preserves_complete_originals_and_source_ids(pipeline, monkeypatch):
|
|
provider = SimpleNamespace(max_tokens=1000, context_window=12000)
|
|
articles = [{'id': i, 'title': f'来源{i}', 'summary': '', 'content': '完整原文。' * 5000} for i in (1, 2)]
|
|
fragments = {1: [], 2: []}
|
|
|
|
async def complete(provider, rules, payload, schema):
|
|
assert pipeline.fits_context(provider, rules, payload, schema)
|
|
identifier, content = payload['article_id'], payload['content']
|
|
fragments[identifier].append(content)
|
|
return pipeline.Extraction(facts='保留事实。', evidence_refs=[
|
|
pipeline.Evidence(article_id=identifier, quote='完整原文。')]), {'input_tokens': 10}
|
|
|
|
monkeypatch.setattr(pipeline, 'complete', complete)
|
|
extracts, usage = asyncio.run(pipeline.extract_articles(provider, '按原文提取事实。', articles))
|
|
for article in articles:
|
|
assert ''.join(fragments[article['id']]) == article['title']+'\n\n'+article['content']
|
|
assert len(fragments[article['id']]) > 1
|
|
assert len(usage) == len(extracts) == sum(len(parts) for parts in fragments.values())
|
|
assert {ref['article_id'] for part in extracts for ref in part['evidence_refs']} == {1, 2}
|