feat(news): index news packages and research persistent events
Extract reusable literal keywords once per source revision, assign complete news packages to long-lived events, and research all event members with immutable evidence and references. Add additive research storage, vocabulary snapshots, the two-stage workspace, isolated acceptance coverage, and a paused rollout with an initial fixed 50-news production trial.
This commit is contained in:
1 parent
07252fea0f
commit
4929a32a0a
69 files changed
+8215
-2092
No files matched your search
@@ -0,0 +1,110 @@
|
||||
"""News retry budgets use synthetic providers only."""
|
||||
import asyncio
|
||||
from types import SimpleNamespace as N
|
||||
from unittest.mock import AsyncMock
|
||||
import httpx
|
||||
import openai
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode', ['success', 'exhausted', 'partial', 'timeout', 'auth', 'long_wait', 'tool_fragment'])
|
||||
def test_news_transport_retry_budget_and_output_guard(trading, monkeypatch, mode):
|
||||
from alphapulse.ai import llm
|
||||
from alphapulse.news_risk.service import model_stream
|
||||
from alphapulse.news_risk.progress import current
|
||||
calls = []
|
||||
async def adapter(*args):
|
||||
calls.append(args)
|
||||
if mode == 'partial': yield llm.TextDelta('started')
|
||||
if mode == 'tool_fragment': llm._news_output()
|
||||
if mode == 'success' and len(calls) == 3:
|
||||
yield llm.Completion(usage={'input_tokens': 1})
|
||||
return
|
||||
request = httpx.Request('POST', 'https://example.invalid/v1/chat/completions')
|
||||
if mode == 'timeout': raise openai.APITimeoutError(request=request)
|
||||
if mode == 'auth': raise openai.AuthenticationError('denied', response=httpx.Response(401, request=request), body={})
|
||||
if mode == 'long_wait': raise openai.RateLimitError('wait', response=httpx.Response(429, headers={'retry-after': '121'}, request=request), body={})
|
||||
raise openai.APIError('Concurrency limit exceeded for account', request, body={'error': {'message': 'Concurrency limit exceeded for account'}})
|
||||
monkeypatch.setattr(llm, '_stream_openai', adapter)
|
||||
sleep = AsyncMock()
|
||||
monkeypatch.setattr(llm.asyncio, 'sleep', sleep)
|
||||
record = {}
|
||||
async def run():
|
||||
token = current.set(record)
|
||||
try: return [item async for item in model_stream(N(protocol='openai'), '', [], [], 'max')]
|
||||
finally: current.reset(token)
|
||||
if mode == 'success': assert asyncio.run(run())[-1].usage == {'input_tokens': 1}
|
||||
else:
|
||||
with pytest.raises(llm.LlmError): asyncio.run(run())
|
||||
retries = mode in ('success', 'exhausted')
|
||||
assert len(calls) == (3 if retries else 1)
|
||||
assert [call.args[0] for call in sleep.await_args_list] == ([30, 60] if retries else [])
|
||||
assert llm.news_retry.get() is None
|
||||
if retries: assert '2/2' in record['phase']
|
||||
|
||||
|
||||
def test_retry_after_seconds_and_dates(trading):
|
||||
from alphapulse.ai import llm
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from email.utils import format_datetime
|
||||
request = httpx.Request('POST', 'https://example.invalid')
|
||||
for value in ('90', format_datetime(datetime.now(UTC) + timedelta(seconds=95))):
|
||||
error = openai.RateLimitError('wait', response=httpx.Response(429, headers={'retry-after': value}, request=request), body={})
|
||||
assert 89 <= llm._news_retry_delay(error, 0) <= 96
|
||||
|
||||
|
||||
def test_cancelling_backoff_releases_context_and_progress(trading, monkeypatch):
|
||||
from alphapulse.ai import llm
|
||||
from alphapulse.news_risk.service import model_stream
|
||||
from alphapulse.news_risk import progress
|
||||
entered = asyncio.Event()
|
||||
async def adapter(*args):
|
||||
raise openai.APIError('Concurrency limit exceeded', httpx.Request('POST', 'https://example.invalid'), body={})
|
||||
yield
|
||||
async def sleep(delay):
|
||||
entered.set()
|
||||
await asyncio.Event().wait()
|
||||
monkeypatch.setattr(llm, '_stream_openai', adapter)
|
||||
monkeypatch.setattr(llm.asyncio, 'sleep', sleep)
|
||||
async def run():
|
||||
async def consume():
|
||||
try:
|
||||
async for _ in model_stream(N(protocol='openai'), '', [], [], None): pass
|
||||
finally: assert llm.news_retry.get() is None
|
||||
task=asyncio.create_task(progress.execute(('events', 4), consume))
|
||||
await entered.wait()
|
||||
assert '重试' in next(iter(progress.active.values()))['phase']
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError): await task
|
||||
assert not progress.active
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@pytest.mark.parametrize('protocol', ['openai', 'anthropic'])
|
||||
@pytest.mark.parametrize('news', [True, False])
|
||||
def test_sdk_retry_is_disabled_only_for_news(trading, monkeypatch, protocol, news):
|
||||
from alphapulse.ai import llm
|
||||
options=[]
|
||||
class Client:
|
||||
def __init__(self, **kwargs):
|
||||
options.append(kwargs)
|
||||
self.chat=N(completions=N(create=self.create))
|
||||
self.messages=N(create=self.create)
|
||||
async def __aenter__(self): return self
|
||||
async def __aexit__(self, *args): pass
|
||||
async def create(self, **kwargs):
|
||||
async def events():
|
||||
if protocol == 'openai':
|
||||
yield N(usage=None, choices=[N(finish_reason='stop', delta=N(content=None,tool_calls=[]))])
|
||||
else: yield N(type='message_stop')
|
||||
return events()
|
||||
monkeypatch.setattr(llm, 'decrypt', lambda _: 'synthetic')
|
||||
monkeypatch.setattr(llm.openai if protocol == 'openai' else llm.anthropic,
|
||||
'AsyncOpenAI' if protocol == 'openai' else 'AsyncAnthropic', Client)
|
||||
provider=N(protocol=protocol,api_key_encrypted='test',base_url='https://example.invalid',model='test',max_tokens=10)
|
||||
async def run():
|
||||
token=llm.news_retry.set({'output':False} if news else None)
|
||||
try: return [item async for item in llm.stream(provider,'rules',[],[],None)]
|
||||
finally: llm.news_retry.reset(token)
|
||||
assert asyncio.run(run())[-1] == llm.Completion()
|
||||
assert options[0]['max_retries'] == (0 if news else llm.MAX_RETRIES)
|
||||
Reference in new issue
Block a user