mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
Integrate lab fff55a78 into PR #40 (cc25d5ba). Lab's modular email backend/frontend, modular settings, split stylesheets (static/style.css stays deleted), procfs compatibility, and request-scoped TurnContract authority win; PR #40's routing classifiers, editor/email/task features, and style.css changes are ported into lab's module and stylesheet homes. Integration fixes: - settings/api.js imports ui.js under its canonical versioned URL - browser observations keep legacy CAPTCHA/access-block evidence - artifact turns do not re-trigger broad-web research recovery - env reference documents PR test-tool variables; page regenerated PR #40 defects surfaced by lab gates and fixed here: - web_fetch generic schema drops top-level anyOf (OpenAI contract); the compact preview contract still requires url or urls - get_weather registered as a brokered network read - new lazy editor modules precached for offline use - SearXNG pin mirrored into GPU standalone compose files - image model picker again skips offline endpoints Tests updated where PR #40 changed behaviour on purpose, and PR tests moved onto lab's document_source helpers.
305 lines
16 KiB
Python
305 lines
16 KiB
Python
"""Writing-menu source text must not redirect the requested editor operation."""
|
|
import re
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from src.clean_agent_preview import (
|
|
targets_active_editor, active_editor_suggestion_request, scope_active_editor_contract,
|
|
required_active_editor_tool_choice,
|
|
)
|
|
from src.turn_contract import (
|
|
editor_request_instructions, requested_capabilities, selected_tools_for_request,
|
|
preserve_bound_editor_selected_tools, resolve_turn_contract,
|
|
)
|
|
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
|
|
from src.tool_policy import ToolPolicy
|
|
from tests.helpers.document_source import declaration
|
|
|
|
MENU = declaration('_AI_WRITING_ACTIONS')
|
|
ACTIONS = dict(re.findall(r"\s+(\w+): '([^']+)'", MENU))
|
|
|
|
|
|
@pytest.mark.parametrize('action', ['proofread', 'improve', 'concise', 'style', 'sources'])
|
|
@pytest.mark.parametrize('newline', ['\n', '\r\n'])
|
|
def test_writing_actions_keep_inline_tools_despite_source_topics(action, newline):
|
|
prompt = ACTIONS[action] + '\n\nUse this configured writing style as the source of truth:\n---\nUse clear sentences about files and tasks.\n---'
|
|
prompt += '\n\nImportant scope: work only on this selected passage. Selected passage:\n---\nThe calendar lists events. I read notes about Python scripts, images and a new document. This sentnce needs help.\n---'
|
|
prompt = prompt.replace('\n', newline)
|
|
doc = SimpleNamespace(title='Essay', language='markdown', current_content='This sentnce needs help.')
|
|
families = requested_capabilities(prompt, active_document=True)
|
|
assert families == ({'documents', 'search_browser'} if action == 'sources' else {'documents'})
|
|
assert targets_active_editor(doc, prompt)
|
|
assert active_editor_suggestion_request(doc, prompt) == (action != 'proofread')
|
|
selected = preserve_bound_editor_selected_tools(prompt, selected_tools_for_request(prompt), active_document=True)
|
|
contract = resolve_turn_contract(capabilities=families, schemas=FUNCTION_TOOL_SCHEMAS,
|
|
policy=ToolPolicy(), selected_tools=selected)
|
|
scoped = scope_active_editor_contract(contract, suggestion_only=action != 'proofread', source_verification=action == 'sources')
|
|
names = {s['function']['name'] for s in scoped.schemas()}
|
|
if action == 'proofread':
|
|
assert names == {'edit_document', 'update_document'}
|
|
return
|
|
assert 'suggest_document' in names
|
|
assert not names & {'edit_document', 'update_document', 'create_document', 'bash'}
|
|
if action == 'sources':
|
|
assert 'web_search' in names
|
|
else:
|
|
assert names == {'suggest_document'}
|
|
assert required_active_editor_tool_choice(active_editor_target=True, suggestion_target=True,
|
|
whole_draft_target=False, offered=scoped.schemas())['function']['name'] == 'suggest_document'
|
|
|
|
|
|
def test_source_boundary_preserves_trailing_instructions_and_plain_requests():
|
|
prompt = 'Proofread the open document. Selected passage:\n---\nCreate a new email.\n---\nAlso check sources online.'
|
|
assert 'Create a new email' not in editor_request_instructions(prompt)
|
|
assert 'Also check sources online.' in editor_request_instructions(prompt)
|
|
assert editor_request_instructions('Create a new email.') == 'Create a new email.'
|
|
|
|
|
|
def test_editor_requests_do_not_capture_explicit_other_targets():
|
|
doc = SimpleNamespace(title='Essay', language='markdown', current_content='Text')
|
|
for prompt in ['Write a note', 'Create a new document', 'Edit my calendar event']:
|
|
assert not targets_active_editor(doc, prompt)
|
|
|
|
|
|
@pytest.mark.parametrize('prompt', ['Fix the spelling in the open document.',
|
|
'Correct this sentence.', 'Polish this paragraph.',
|
|
'Shorten the open document.', 'Rewrite this document.'])
|
|
def test_direct_edits_share_the_capability_router(prompt):
|
|
doc = SimpleNamespace(title='Essay', language='markdown', current_content='Text')
|
|
assert requested_capabilities(prompt, active_document=True) == {'documents'}
|
|
assert targets_active_editor(doc, prompt)
|
|
assert not active_editor_suggestion_request(doc, prompt)
|
|
|
|
|
|
def test_source_review_still_requires_tool_completion_after_research():
|
|
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] in {'web_search', 'suggest_document'}]
|
|
assert required_active_editor_tool_choice(active_editor_target=True, suggestion_target=True,
|
|
whole_draft_target=False, offered=schemas) == 'required'
|
|
|
|
|
|
@pytest.mark.parametrize('value', [3, None, [], 'not an object'])
|
|
def test_malformed_editor_tool_arguments_get_a_recoverable_error(value):
|
|
from src.clean_agent_preview import normalize_preview_call_args
|
|
with pytest.raises(ValueError, match='JSON object'):
|
|
normalize_preview_call_args('suggest_document', value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('tool,prompt,args', [
|
|
('suggest_document', 'Proofread the open document. Create inline suggestions only; do not apply changes.',
|
|
{'suggestions': [{'find': 'A sentnce.', 'replace': 'A sentence.', 'reason': 'Spelling.'}],
|
|
'more': True}),
|
|
('edit_document', 'Fix the spelling in the open document.',
|
|
{'edits': [{'find': 'A sentnce.', 'replace': 'A sentence.'}]}),
|
|
('update_document', 'Rewrite the whole open document.', {'content': 'A sentence.'}),
|
|
])
|
|
async def test_editor_loop_recovers_scalar_arguments_and_emits_editor_event(monkeypatch, tool, prompt, args):
|
|
import json
|
|
import src.clean_agent_preview as runner
|
|
from src.turn_contract import resolve_full_inventory_contract
|
|
requests, executed = [], []
|
|
deltas = iter([
|
|
{'content': 'PREMATURE_SUCCESS', 'tool_calls': [{'index': 0, 'id': 'bad', 'function': {'name': tool, 'arguments': '123'}}]},
|
|
{'tool_calls': [{'index': 0, 'id': 'good', 'function': {'name': tool, 'arguments': json.dumps(args)}}]},
|
|
{'content': 'Done.'},
|
|
])
|
|
class Response:
|
|
def __init__(self, delta): self.delta = delta
|
|
async def __aenter__(self): return self
|
|
async def __aexit__(self, *args): pass
|
|
def raise_for_status(self): pass
|
|
async def aiter_lines(self):
|
|
yield 'data: ' + json.dumps({'choices': [{'delta': self.delta}]})
|
|
yield 'data: [DONE]'
|
|
class Client:
|
|
def __init__(self, **kwargs): pass
|
|
async def __aenter__(self): return self
|
|
async def __aexit__(self, *args): pass
|
|
def stream(self, *args, **kwargs):
|
|
requests.append(kwargs['json'])
|
|
return Response(next(deltas))
|
|
async def execute(block, **kwargs):
|
|
executed.append(block)
|
|
return tool, {'exit_code': 0, 'action': 'suggest' if tool == 'suggest_document' else 'update',
|
|
'doc_id': 'fixture', 'content': 'A sentence.', 'title': 'Fixture',
|
|
'language': 'markdown', 'version': 2,
|
|
'suggestions': args.get('suggestions', [])}
|
|
import src.email_task_intent as intent_module
|
|
async def classify(*args, **kwargs):
|
|
return intent_module.EmailTaskIntent('other', (), 'Edit the open document')
|
|
monkeypatch.setattr(intent_module, 'classify_email_task', classify)
|
|
monkeypatch.setattr(runner.httpx, 'AsyncClient', Client)
|
|
monkeypatch.setattr(runner, 'execute_tool_block', execute)
|
|
policy = ToolPolicy()
|
|
# Preserve both direct writers; model chooses between targeted and full edit.
|
|
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] in
|
|
{'suggest_document', 'edit_document', 'update_document'}]
|
|
contract = resolve_full_inventory_contract(schemas=schemas, policy=policy)
|
|
doc = SimpleNamespace(id='fixture', title='Fixture', language='markdown', current_content='A sentnce.')
|
|
events = [json.loads(c[6:]) async for c in runner.stream_preview(
|
|
endpoint_url='http://test', model='Ajax', messages=[{'role': 'user', 'content': prompt}],
|
|
headers={}, turn_contract=contract, session_id='fixture', owner='fixture',
|
|
disabled_tools=set(), tool_policy=policy, active_document=doc, max_tokens=4096,
|
|
) if '[DONE]' not in c]
|
|
assert len(executed) == 1
|
|
assert requests[-1]["max_tokens"] == 256
|
|
assert not requests[-1].get("tools")
|
|
assert requests[0]['max_tokens'] == 4096
|
|
assert requests[0]['tool_choice'] == 'auto'
|
|
assert not any('PREMATURE_SUCCESS' in e.get('delta', '') for e in events)
|
|
assert requests[0]['parallel_tool_calls'] is False
|
|
assert 'parallel_tool_calls' not in requests[-1]
|
|
outputs = [e for e in events if e.get('type') == 'tool_output']
|
|
assert any(e.get('type') == 'editor_progress' for e in events)
|
|
assert outputs[0]['execution_attempted'] is False
|
|
assert 'JSON object' in outputs[0]['output']
|
|
assert outputs[1]['error'] is False
|
|
if tool == 'suggest_document':
|
|
assert any(e.get('type') == 'doc_suggestions' for e in events)
|
|
else:
|
|
assert any(e.get('type') == 'doc_update' for e in events)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finish_after_saved_editor_batch_skips_another_model_request(monkeypatch):
|
|
import json
|
|
import src.clean_agent_preview as runner
|
|
from src.turn_contract import resolve_full_inventory_contract
|
|
|
|
requests, executed = [], []
|
|
args = {'edits': [{'find': 'A sentnce.', 'replace': 'A sentence.'}], 'more': True}
|
|
|
|
class Response:
|
|
async def __aenter__(self): return self
|
|
async def __aexit__(self, *ignored): pass
|
|
def raise_for_status(self): pass
|
|
async def aiter_lines(self):
|
|
yield 'data: ' + json.dumps({'choices': [{'delta': {'tool_calls': [
|
|
{'index': 0, 'id': 'edit', 'function': {
|
|
'name': 'edit_document', 'arguments': json.dumps(args)}}
|
|
]}}]})
|
|
yield 'data: [DONE]'
|
|
|
|
class Client:
|
|
def __init__(self, **ignored): pass
|
|
async def __aenter__(self): return self
|
|
async def __aexit__(self, *ignored): pass
|
|
def stream(self, *ignored, **kwargs):
|
|
requests.append(kwargs['json'])
|
|
return Response()
|
|
|
|
async def execute(block, **ignored):
|
|
executed.append(block)
|
|
return 'edit_document', {'exit_code': 0, 'action': 'edit', 'doc_id': 'fixture',
|
|
'content': 'A sentence.', 'title': 'Fixture',
|
|
'language': 'markdown', 'version': 2, 'applied': 1}
|
|
|
|
import src.email_task_intent as intent_module
|
|
async def classify(*ignored, **kwargs):
|
|
return intent_module.EmailTaskIntent('other', (), 'Edit the open document')
|
|
monkeypatch.setattr(intent_module, 'classify_email_task', classify)
|
|
monkeypatch.setattr(runner.httpx, 'AsyncClient', Client)
|
|
monkeypatch.setattr(runner, 'execute_tool_block', execute)
|
|
monkeypatch.setattr(runner.agent_runs, 'should_finish', lambda _session: bool(executed))
|
|
policy = ToolPolicy()
|
|
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'edit_document']
|
|
contract = resolve_full_inventory_contract(schemas=schemas, policy=policy)
|
|
doc = SimpleNamespace(id='fixture', title='Fixture', language='markdown', current_content='A sentnce.')
|
|
events = [json.loads(chunk[6:]) async for chunk in runner.stream_preview(
|
|
endpoint_url='http://test', model='Ajax', messages=[{'role': 'user', 'content': 'Fix the spelling in the open document.'}],
|
|
headers={}, turn_contract=contract, session_id='fixture', owner='fixture',
|
|
disabled_tools=set(), tool_policy=policy, active_document=doc, max_tokens=4096,
|
|
) if chunk.startswith('data: ') and '[DONE]' not in chunk]
|
|
assert len(requests) == 1
|
|
assert len(executed) == 1
|
|
assert any(e.get('type') == 'doc_update' for e in events)
|
|
assert any(e.get('type') == 'final_response' and 'saved so far' in e.get('content', '')
|
|
for e in events)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finish_interrupts_wait_for_next_model_token():
|
|
import asyncio
|
|
from src.clean_agent_preview import preview_lines_until_finish
|
|
|
|
finish = asyncio.Event()
|
|
waiting = asyncio.Event()
|
|
released = asyncio.Event()
|
|
|
|
class SlowResponse:
|
|
async def aiter_lines(self):
|
|
waiting.set()
|
|
try:
|
|
await asyncio.Event().wait()
|
|
yield 'unreachable'
|
|
finally:
|
|
released.set()
|
|
|
|
task = asyncio.create_task(_collect_preview_lines(SlowResponse(), finish))
|
|
await asyncio.wait_for(waiting.wait(), 1)
|
|
finish.set()
|
|
assert await asyncio.wait_for(task, 1) == []
|
|
assert released.is_set()
|
|
|
|
|
|
async def _collect_preview_lines(response, finish):
|
|
from src.clean_agent_preview import preview_lines_until_finish
|
|
return [line async for line in preview_lines_until_finish(response, finish)]
|
|
|
|
|
|
def test_selection_only_edit_cannot_replace_all_occurrences():
|
|
from src.clean_agent_preview import normalize_preview_call_args
|
|
with pytest.raises(ValueError, match='Selection-only'):
|
|
normalize_preview_call_args('edit_document', {'edits': [
|
|
{'find': 'typo', 'replace': 'word', 'replace_all': True}]},
|
|
user_text='Important scope: work only on this selected passage.')
|
|
|
|
|
|
def test_concise_suggestions_must_actually_shorten_prose():
|
|
from src.clean_agent_preview import document_suggestion_quality_error
|
|
prompt = ACTIONS['concise']
|
|
original = '<p>This sentnce has an unecessary delay.</p>'
|
|
spelling_only = '<p>This sentence has an unnecessary delay.</p>'
|
|
proposed = {'suggestions': [{'find': original, 'replace': spelling_only}]}
|
|
assert 'does not make its passage more concise' in document_suggestion_quality_error(
|
|
'suggest_document', proposed, user_text=prompt)
|
|
proposed['suggestions'][0]['replace'] = '<p>This sentence drags.</p>'
|
|
assert document_suggestion_quality_error('suggest_document', proposed, user_text=prompt) is None
|
|
proposed['suggestions'][0]['replace'] = spelling_only
|
|
assert document_suggestion_quality_error('suggest_document', proposed,
|
|
user_text='Proofread the open document.') is None
|
|
|
|
|
|
def test_ajax_editor_schema_limits_batches_and_exposes_continuation():
|
|
from src.clean_agent_preview import compact_schemas
|
|
schemas = compact_schemas([s for s in FUNCTION_TOOL_SCHEMAS
|
|
if s['function']['name'] in {'edit_document', 'suggest_document'}], model='Ajax')
|
|
by_name = {s['function']['name']: s['function']['parameters']['properties'] for s in schemas}
|
|
assert by_name['edit_document']['edits']['maxItems'] == 12
|
|
assert by_name['suggest_document']['suggestions']['maxItems'] == 12
|
|
assert by_name['edit_document']['more']['type'] == 'boolean'
|
|
assert by_name['suggest_document']['more']['type'] == 'boolean'
|
|
|
|
|
|
def test_exact_edits_continue_but_suggestions_finish_after_one_bounded_set():
|
|
from src.clean_agent_preview import editor_batch_continues
|
|
assert editor_batch_continues('edit_document', {'edits': [{}] * 12})
|
|
assert not editor_batch_continues('suggest_document', {'suggestions': [{}] * 12})
|
|
assert not editor_batch_continues('suggest_document', {'suggestions': [{}], 'more': True})
|
|
assert not editor_batch_continues('edit_document', {'edits': [{}] * 11})
|
|
assert not editor_batch_continues('edit_document', {'edits': [{}] * 12, 'more': False})
|
|
assert editor_batch_continues('edit_document', {'edits': [{}], 'more': True})
|
|
|
|
|
|
def test_noop_sibling_does_not_create_a_failed_tool_card_after_a_real_edit():
|
|
import json
|
|
from src.clean_agent_preview import drop_redundant_editor_noops
|
|
good = {'function': {'name': 'edit_document', 'arguments': json.dumps({
|
|
'edits': [{'find': 'old', 'replace': 'new'}]})}}
|
|
noop = {'function': {'name': 'edit_document', 'arguments': json.dumps({
|
|
'edits': [{'find': 'already correct', 'replace': 'already correct'}]})}}
|
|
assert drop_redundant_editor_noops([good, noop]) == [good]
|
|
assert drop_redundant_editor_noops([noop]) == [noop]
|
|
assert drop_redundant_editor_noops([good, good]) == [good]
|