Bound research context and audit source-grounded output corrections
Development CI / check-and-build (push) Successful in 4m55s
GitHub Git mirror / mirror (push) Successful in 6s
Development CI / deploy-development (push) Successful in 2m30s

This commit is contained in:
eezd committed 2026-11-06 10:52:02 +08:00
1 parent 4f4db88333
commit 2cc5598ebd
6 files changed
+186 -22

No files matched your search

+72
View File
@@ -174,3 +174,75 @@ def test_oversized_scope_fails_before_model_call_with_article_identity(pipeline,
asyncio.run(pipeline.extract_articles(SimpleNamespace(max_tokens=100, context_window=1500), '规则',
[{'id': 9, 'title': '标题', 'content': '正文'}], scopes=[{'news_id': 9, 'scope_quote': '长' * 5000}]))
complete.assert_not_awaited()
def test_alias_cleanup_preserves_tags_valid_aliases_and_raw_output(trading):
from alphapulse.news_research.contracts import IndexResult
from alphapulse.news_research.service import clean_aliases, validate_keywords
raw = IndexResult(keywords=[dict(canonical='插件调用计费', aliases=['插件调用','插件调用收费'], quote='插件调用单独计费')])
source = {'id':1, 'content':'插件调用单独计费'}
output, changes = clean_aliases(raw, source)
validate_keywords(output, source)
assert output.keywords[0].aliases == ['插件调用']
assert raw.keywords[0].aliases == ['插件调用','插件调用收费']
assert changes[0]['original'] == raw.keywords[0].aliases
with pytest.raises(ValueError, match='没有任何原文有效别名'):
clean_aliases(raw, {'id':1,'content':'其他文字'})
@pytest.mark.parametrize('source,quote,corrected', [
('官员“感到担忧”。','官员“感到担忧” 。',True),
('官员“感到担忧”。官员“感到担忧”。','官员“感到担忧” 。',False),
('金额100美元。','金额1000美元。',False),
('US D company.','USD company.',False),
])
def test_quote_alignment_only_recovers_unique_punctuation_whitespace(pipeline, source, quote, corrected):
value = {'claims':[{'evidence_refs':[{'article_id':-2,'quote':quote}]}]}
changes = pipeline.align_quotes(value, {-2:{'content':source}})
assert bool(changes) == corrected
if corrected:
pipeline.validate_evidence([pipeline.Evidence.model_validate(value['claims'][0]['evidence_refs'][0])], {-2:{'content':source}})
assert changes[0]['original'] == quote
else:
assert value['claims'][0]['evidence_refs'][0]['quote'] == quote
def test_oversized_tool_response_is_not_delivered_or_registered(trading, monkeypatch):
from alphapulse.news_research.tools import Research
from alphapulse.news_research import model
from alphapulse.ai.llm import Completion
from pydantic import BaseModel
class Result(BaseModel):
facts: str
reader = Research()
calls = []
async def stream(provider, system, messages, tools, effort):
calls.append(messages.copy())
if len(calls) == 1:
yield Completion(tool_calls=[{'id':'1','name':'read_news','arguments':{'id':99}}],usage={'input_tokens':10})
else:
assert [t.name for t in tools] == ['submit']
assert 'context_limit' in messages[-1]['result']
assert 'x'*100 not in messages[-1]['result']
yield Completion(tool_calls=[{'id':'2','name':'submit','arguments':{'facts':'证据不足'}}],usage={'input_tokens':20})
async def tool(name, args):
result = {'article_id':-1,'content':'x'*20000}
reader.sources[-1] = result
reader.provided_news[99] = -1
reader.records.append({'tool':name,'arguments':args,'result':result})
return result
monkeypatch.setattr(reader, 'tool', tool)
monkeypatch.setattr(model.llm, 'stream', stream)
output, usage = asyncio.run(reader.run(SimpleNamespace(model='test',reasoning_effort='medium',max_tokens=100,context_window=8000), '规则', {}, Result))
assert output.facts == '证据不足' and len(usage['rounds']) == 2
assert not reader.sources and not reader.provided_news
assert reader.records[0]['result']['status'] == 'context_limit'
assert len(reader.records[0]['undelivered_result']['content']) == 20000
def test_undelivered_source_is_not_restored_as_read(trading):
from alphapulse.news_research.tools import Research
reader = Research()
reader.restore([{'tool':'read_news','arguments':{'id':99},
'result':{'status':'context_limit'},'undelivered_result':{'article_id':-1,'content':'原文'}}])
assert not reader.sources and not reader.cache and not reader.provided_news
@@ -221,7 +221,7 @@ def test_retry_refreshes_local_search_with_current_vocabulary(
session = AsyncMock()
session.__aenter__.return_value = session
session.execute.return_value = [
(N(id=2, news_id=12, content_hash="current", keywords=[]), "current")
(N(id=2, news_id=12, content_hash="current", keywords=[]), "current", "Fresh news")
]
session.scalars.return_value = [N(id=2, title="Fresh event", tags=["binance"])]
monkeypatch.setattr(service, "SessionLocal", lambda: session)