mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-18 23:11:12 +02:00
refactor: streamline TikTok and Instagram scraping logic by removing search_queries and enhancing documentation for clarity
This commit is contained in:
parent
e8b3692b54
commit
2b018c4474
111 changed files with 1800 additions and 1580 deletions
|
|
@ -52,9 +52,7 @@ async def test_acquire_token_local_mode_posts_desktop_login_json():
|
|||
200, json={"access_token": "T", "refresh_token": "R", "token_type": "bearer"}
|
||||
)
|
||||
)
|
||||
config = _make_config(
|
||||
surfsense_user_email="u@example.com", surfsense_user_password="pw"
|
||||
)
|
||||
config = _make_config(surfsense_user_email="u@example.com", surfsense_user_password="pw")
|
||||
bundle = await acquire_token(config)
|
||||
assert bundle.access_token == "T"
|
||||
assert bundle.refresh_token == "R"
|
||||
|
|
|
|||
|
|
@ -94,10 +94,18 @@ async def test_documents_status_parses_state(respx_mock, http):
|
|||
200,
|
||||
json={
|
||||
"items": [
|
||||
{"id": 1, "title": "a.pdf", "document_type": "FILE",
|
||||
"status": {"state": "ready", "reason": None}},
|
||||
{"id": 2, "title": "b.pdf", "document_type": "FILE",
|
||||
"status": {"state": "failed", "reason": "ETL boom"}},
|
||||
{
|
||||
"id": 1,
|
||||
"title": "a.pdf",
|
||||
"document_type": "FILE",
|
||||
"status": {"state": "ready", "reason": None},
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "b.pdf",
|
||||
"document_type": "FILE",
|
||||
"status": {"state": "failed", "reason": "ETL boom"},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
|
@ -137,14 +145,26 @@ async def test_documents_upload_returns_payload(respx_mock, http, tmp_path: Path
|
|||
async def test_documents_list_chunks_paginated(respx_mock, http):
|
||||
respx_mock.get("/api/v1/documents/5/chunks").mock(
|
||||
side_effect=[
|
||||
httpx.Response(200, json={
|
||||
"items": [{"id": 1, "content": "a"}, {"id": 2, "content": "b"}],
|
||||
"total": 3, "page": 0, "page_size": 2, "has_more": True,
|
||||
}),
|
||||
httpx.Response(200, json={
|
||||
"items": [{"id": 3, "content": "c"}],
|
||||
"total": 3, "page": 1, "page_size": 2, "has_more": False,
|
||||
}),
|
||||
httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"items": [{"id": 1, "content": "a"}, {"id": 2, "content": "b"}],
|
||||
"total": 3,
|
||||
"page": 0,
|
||||
"page_size": 2,
|
||||
"has_more": True,
|
||||
},
|
||||
),
|
||||
httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"items": [{"id": 3, "content": "c"}],
|
||||
"total": 3,
|
||||
"page": 1,
|
||||
"page_size": 2,
|
||||
"has_more": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
client = DocumentsClient(http, _BASE)
|
||||
|
|
@ -191,15 +211,17 @@ def _sse_body(events: list[dict]) -> bytes:
|
|||
@pytest.mark.asyncio
|
||||
@respx.mock(base_url=_BASE)
|
||||
async def test_ask_accumulates_text_deltas(respx_mock, http):
|
||||
body = _sse_body([
|
||||
{"type": "start", "messageId": "m1"},
|
||||
{"type": "text-start", "id": "t1"},
|
||||
{"type": "text-delta", "id": "t1", "delta": "Answer "},
|
||||
{"type": "text-delta", "id": "t1", "delta": "is "},
|
||||
{"type": "text-delta", "id": "t1", "delta": "B [citation:42]."},
|
||||
{"type": "text-end", "id": "t1"},
|
||||
{"type": "finish"},
|
||||
])
|
||||
body = _sse_body(
|
||||
[
|
||||
{"type": "start", "messageId": "m1"},
|
||||
{"type": "text-start", "id": "t1"},
|
||||
{"type": "text-delta", "id": "t1", "delta": "Answer "},
|
||||
{"type": "text-delta", "id": "t1", "delta": "is "},
|
||||
{"type": "text-delta", "id": "t1", "delta": "B [citation:42]."},
|
||||
{"type": "text-end", "id": "t1"},
|
||||
{"type": "finish"},
|
||||
]
|
||||
)
|
||||
respx_mock.post("/api/v1/new_chat").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
|
|
@ -208,9 +230,7 @@ async def test_ask_accumulates_text_deltas(respx_mock, http):
|
|||
)
|
||||
)
|
||||
client = NewChatClient(http, _BASE)
|
||||
answer = await client.ask(
|
||||
thread_id=1, search_space_id=2, user_query="What is the answer?"
|
||||
)
|
||||
answer = await client.ask(thread_id=1, search_space_id=2, user_query="What is the answer?")
|
||||
assert answer.text == "Answer is B [citation:42]."
|
||||
assert answer.finished_normally is True
|
||||
assert any(c["chunk_id"] == 42 for c in answer.citations)
|
||||
|
|
@ -219,23 +239,21 @@ async def test_ask_accumulates_text_deltas(respx_mock, http):
|
|||
@pytest.mark.asyncio
|
||||
@respx.mock(base_url=_BASE)
|
||||
async def test_ask_409_thread_busy_retries(respx_mock, http):
|
||||
body = _sse_body([
|
||||
{"type": "text-delta", "id": "t1", "delta": "ok"},
|
||||
{"type": "finish"},
|
||||
])
|
||||
body = _sse_body(
|
||||
[
|
||||
{"type": "text-delta", "id": "t1", "delta": "ok"},
|
||||
{"type": "finish"},
|
||||
]
|
||||
)
|
||||
busy = httpx.Response(
|
||||
409,
|
||||
json={"detail": {"errorCode": "THREAD_BUSY", "message": "busy"}},
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
success = httpx.Response(
|
||||
200, content=body, headers={"Content-Type": "text/event-stream"}
|
||||
)
|
||||
success = httpx.Response(200, content=body, headers={"Content-Type": "text/event-stream"})
|
||||
respx_mock.post("/api/v1/new_chat").mock(side_effect=[busy, success])
|
||||
client = NewChatClient(http, _BASE)
|
||||
answer = await client.ask(
|
||||
thread_id=1, search_space_id=2, user_query="hi", max_busy_retries=2
|
||||
)
|
||||
answer = await client.ask(thread_id=1, search_space_id=2, user_query="hi", max_busy_retries=2)
|
||||
assert answer.text == "ok"
|
||||
|
||||
|
||||
|
|
@ -250,6 +268,4 @@ async def test_ask_409_exhausts_retries(respx_mock, http):
|
|||
respx_mock.post("/api/v1/new_chat").mock(return_value=busy)
|
||||
client = NewChatClient(http, _BASE)
|
||||
with pytest.raises(ThreadBusyError):
|
||||
await client.ask(
|
||||
thread_id=1, search_space_id=2, user_query="hi", max_busy_retries=1
|
||||
)
|
||||
await client.ask(thread_id=1, search_space_id=2, user_query="hi", max_busy_retries=1)
|
||||
|
|
|
|||
|
|
@ -46,32 +46,24 @@ class TestMerge:
|
|||
|
||||
def test_explicit_false_overrides_default_true(self) -> None:
|
||||
defaults = IngestSettings(use_vision_llm=True)
|
||||
merged = IngestSettings.merge(
|
||||
defaults, {"use_vision_llm": False}
|
||||
)
|
||||
merged = IngestSettings.merge(defaults, {"use_vision_llm": False})
|
||||
assert merged.use_vision_llm is False
|
||||
|
||||
def test_explicit_true_overrides_default_false(self) -> None:
|
||||
defaults = IngestSettings(use_vision_llm=False)
|
||||
merged = IngestSettings.merge(
|
||||
defaults, {"use_vision_llm": True}
|
||||
)
|
||||
merged = IngestSettings.merge(defaults, {"use_vision_llm": True})
|
||||
assert merged.use_vision_llm is True
|
||||
|
||||
def test_none_means_silent(self) -> None:
|
||||
# Argparse with BooleanOptionalAction yields None when the
|
||||
# operator passed neither --use-vision-llm nor --no-vision-llm.
|
||||
defaults = IngestSettings(use_vision_llm=True)
|
||||
merged = IngestSettings.merge(
|
||||
defaults, {"use_vision_llm": None}
|
||||
)
|
||||
merged = IngestSettings.merge(defaults, {"use_vision_llm": None})
|
||||
assert merged.use_vision_llm is True
|
||||
|
||||
def test_processing_mode_override(self) -> None:
|
||||
defaults = IngestSettings(processing_mode="basic")
|
||||
merged = IngestSettings.merge(
|
||||
defaults, {"processing_mode": "premium"}
|
||||
)
|
||||
merged = IngestSettings.merge(defaults, {"processing_mode": "premium"})
|
||||
assert merged.processing_mode == "premium"
|
||||
|
||||
def test_processing_mode_invalid_raises(self) -> None:
|
||||
|
|
@ -134,9 +126,7 @@ class TestAddArgs:
|
|||
p = argparse.ArgumentParser()
|
||||
add_ingest_settings_args(
|
||||
p,
|
||||
defaults=IngestSettings(
|
||||
use_vision_llm=False, processing_mode="basic"
|
||||
),
|
||||
defaults=IngestSettings(use_vision_llm=False, processing_mode="basic"),
|
||||
)
|
||||
return p
|
||||
|
||||
|
|
@ -158,31 +148,21 @@ class TestAddArgs:
|
|||
args = parser.parse_args(["--processing-mode", mode])
|
||||
assert args.processing_mode == mode
|
||||
|
||||
def test_processing_mode_rejects_unknown(
|
||||
self, parser: argparse.ArgumentParser
|
||||
) -> None:
|
||||
def test_processing_mode_rejects_unknown(self, parser: argparse.ArgumentParser) -> None:
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(["--processing-mode", "exotic"])
|
||||
|
||||
def test_vision_flags_mutually_exclusive(
|
||||
self, parser: argparse.ArgumentParser
|
||||
) -> None:
|
||||
def test_vision_flags_mutually_exclusive(self, parser: argparse.ArgumentParser) -> None:
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(["--use-vision-llm", "--no-vision-llm"])
|
||||
|
||||
def test_full_pipeline(self, parser: argparse.ArgumentParser) -> None:
|
||||
# Operator passes flags + defaults are reasonable. Merge
|
||||
# should yield exactly what they asked for.
|
||||
args = parser.parse_args(
|
||||
["--use-vision-llm", "--processing-mode", "premium"]
|
||||
)
|
||||
defaults = IngestSettings(
|
||||
use_vision_llm=False, processing_mode="basic"
|
||||
)
|
||||
args = parser.parse_args(["--use-vision-llm", "--processing-mode", "premium"])
|
||||
defaults = IngestSettings(use_vision_llm=False, processing_mode="basic")
|
||||
merged = IngestSettings.merge(defaults, vars(args))
|
||||
assert merged == IngestSettings(
|
||||
use_vision_llm=True, processing_mode="premium"
|
||||
)
|
||||
assert merged == IngestSettings(use_vision_llm=True, processing_mode="premium")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -240,16 +220,12 @@ class TestHeader:
|
|||
|
||||
class TestFormatMd:
|
||||
def test_full_settings(self) -> None:
|
||||
out = format_ingest_settings_md(
|
||||
{"use_vision_llm": True, "processing_mode": "premium"}
|
||||
)
|
||||
out = format_ingest_settings_md({"use_vision_llm": True, "processing_mode": "premium"})
|
||||
assert "vision_llm=`on`" in out
|
||||
assert "processing_mode=`premium`" in out
|
||||
|
||||
def test_default_off(self) -> None:
|
||||
out = format_ingest_settings_md(
|
||||
{"use_vision_llm": False, "processing_mode": "basic"}
|
||||
)
|
||||
out = format_ingest_settings_md({"use_vision_llm": False, "processing_mode": "basic"})
|
||||
assert "vision_llm=`off`" in out
|
||||
assert "processing_mode=`basic`" in out
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,12 @@ from surfsense_evals.core.metrics import (
|
|||
@pytest.mark.parametrize(
|
||||
"k,n,low,high",
|
||||
[
|
||||
(80, 100, 0.7111, 0.8666), # cross-checked vs statsmodels.proportion_confint(method='wilson')
|
||||
(
|
||||
80,
|
||||
100,
|
||||
0.7111,
|
||||
0.8666,
|
||||
), # cross-checked vs statsmodels.proportion_confint(method='wilson')
|
||||
(50, 100, 0.4038, 0.5962),
|
||||
(0, 0, 0.0, 1.0),
|
||||
(0, 10, 0.0, 0.2775),
|
||||
|
|
@ -74,7 +79,7 @@ def test_mcnemar_exact_branch_strong_signal():
|
|||
assert res.b == 0
|
||||
assert res.c == 10
|
||||
assert res.method == "exact"
|
||||
expected = 2 * (0.5 ** 10)
|
||||
expected = 2 * (0.5**10)
|
||||
assert math.isclose(res.p_value, expected, rel_tol=1e-9)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ from surfsense_evals.core.parse.answer_letter import AnswerLetterResult
|
|||
@pytest.mark.parametrize(
|
||||
"text,expected_letter,expected_strategy",
|
||||
[
|
||||
('```json\n{"step_by_step_thinking": "...", "answer_choice": "B"}\n```', "B", "json_envelope"),
|
||||
(
|
||||
'```json\n{"step_by_step_thinking": "...", "answer_choice": "B"}\n```',
|
||||
"B",
|
||||
"json_envelope",
|
||||
),
|
||||
('Reasoning... {"step_by_step_thinking": "x", "answer_choice": "C"}', "C", "json_envelope"),
|
||||
("Long reasoning.\nAnswer: D", "D", "answer_line"),
|
||||
("The correct answer is (A).", "A", "answer_line"),
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ def test_regex_pattern_matches_ts_source():
|
|||
assert "https?://" in pattern
|
||||
assert "urlcite" in pattern
|
||||
assert "doc-" in pattern
|
||||
assert "\u200B" in pattern
|
||||
assert "\u200b" in pattern
|
||||
assert "【" in pattern and "】" in pattern
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -44,11 +44,14 @@ class TestExtractFreeformAnswer:
|
|||
assert extract_freeform_answer("ANSWER: yes") == "yes"
|
||||
assert extract_freeform_answer("answer: no") == "no"
|
||||
|
||||
@pytest.mark.parametrize("text,expected", [
|
||||
("Answer: 1, 2, 3", "1, 2, 3"),
|
||||
("Answer: 3.14", "3.14"),
|
||||
("Answer: spaced ", "spaced"),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"text,expected",
|
||||
[
|
||||
("Answer: 1, 2, 3", "1, 2, 3"),
|
||||
("Answer: 3.14", "3.14"),
|
||||
("Answer: spaced ", "spaced"),
|
||||
],
|
||||
)
|
||||
def test_various_payloads(self, text: str, expected: str) -> None:
|
||||
assert extract_freeform_answer(text) == expected
|
||||
|
||||
|
|
|
|||
|
|
@ -22,12 +22,16 @@ async def _astream(lines):
|
|||
@pytest.mark.asyncio
|
||||
async def test_basic_data_frame():
|
||||
events = await _alist(
|
||||
iter_sse_events(_astream([
|
||||
'data: {"type": "text-delta", "delta": "hi"}',
|
||||
"",
|
||||
'data: {"type": "finish"}',
|
||||
"",
|
||||
]))
|
||||
iter_sse_events(
|
||||
_astream(
|
||||
[
|
||||
'data: {"type": "text-delta", "delta": "hi"}',
|
||||
"",
|
||||
'data: {"type": "finish"}',
|
||||
"",
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert [e.data for e in events] == [
|
||||
'{"type": "text-delta", "delta": "hi"}',
|
||||
|
|
@ -38,10 +42,14 @@ async def test_basic_data_frame():
|
|||
@pytest.mark.asyncio
|
||||
async def test_done_sentinel_passes_through():
|
||||
events = await _alist(
|
||||
iter_sse_events(_astream([
|
||||
"data: [DONE]",
|
||||
"",
|
||||
]))
|
||||
iter_sse_events(
|
||||
_astream(
|
||||
[
|
||||
"data: [DONE]",
|
||||
"",
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert [e.data for e in events] == ["[DONE]"]
|
||||
|
||||
|
|
@ -49,11 +57,15 @@ async def test_done_sentinel_passes_through():
|
|||
@pytest.mark.asyncio
|
||||
async def test_multiline_data_joins_with_newline():
|
||||
events = await _alist(
|
||||
iter_sse_events(_astream([
|
||||
"data: line1",
|
||||
"data: line2",
|
||||
"",
|
||||
]))
|
||||
iter_sse_events(
|
||||
_astream(
|
||||
[
|
||||
"data: line1",
|
||||
"data: line2",
|
||||
"",
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert events[0].data == "line1\nline2"
|
||||
|
||||
|
|
@ -61,13 +73,17 @@ async def test_multiline_data_joins_with_newline():
|
|||
@pytest.mark.asyncio
|
||||
async def test_comments_and_other_fields_ignored():
|
||||
events = await _alist(
|
||||
iter_sse_events(_astream([
|
||||
": heartbeat",
|
||||
"event: foo",
|
||||
"id: 123",
|
||||
"data: payload",
|
||||
"",
|
||||
]))
|
||||
iter_sse_events(
|
||||
_astream(
|
||||
[
|
||||
": heartbeat",
|
||||
"event: foo",
|
||||
"id: 123",
|
||||
"data: payload",
|
||||
"",
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert [e.data for e in events] == ["payload"]
|
||||
|
||||
|
|
@ -77,8 +93,12 @@ async def test_handles_missing_trailing_blank():
|
|||
"""Some servers omit the final blank line; the consumer should still emit."""
|
||||
|
||||
events = await _alist(
|
||||
iter_sse_events(_astream([
|
||||
"data: only-one",
|
||||
]))
|
||||
iter_sse_events(
|
||||
_astream(
|
||||
[
|
||||
"data: only-one",
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert [e.data for e in events] == ["only-one"]
|
||||
|
|
|
|||
|
|
@ -36,11 +36,18 @@ async def test_payload_shape_matches_openrouter_docs(respx_mock, tiny_pdf: Path)
|
|||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"choices": [{
|
||||
"message": {"content": "Answer: B"},
|
||||
"finish_reason": "stop",
|
||||
}],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15, "cost": 0.0001},
|
||||
"choices": [
|
||||
{
|
||||
"message": {"content": "Answer: B"},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 5,
|
||||
"total_tokens": 15,
|
||||
"cost": 0.0001,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -63,8 +70,7 @@ async def test_payload_shape_matches_openrouter_docs(respx_mock, tiny_pdf: Path)
|
|||
assert file_part["file"]["filename"] == tiny_pdf.name
|
||||
assert file_part["file"]["file_data"].startswith("data:application/pdf;base64,")
|
||||
assert (
|
||||
base64.b64decode(file_part["file"]["file_data"].split(",", 1)[1])
|
||||
== tiny_pdf.read_bytes() # noqa: ASYNC240 — test fixture, sync read is fine
|
||||
base64.b64decode(file_part["file"]["file_data"].split(",", 1)[1]) == tiny_pdf.read_bytes() # noqa: ASYNC240 — test fixture, sync read is fine
|
||||
)
|
||||
assert user["content"][1] == {"type": "text", "text": "What is the diagnosis?"}
|
||||
assert captured["headers"]["authorization"] == "Bearer sk-or-test"
|
||||
|
|
@ -85,22 +91,22 @@ async def test_chat_array_content_concatenates(respx_mock, tiny_pdf: Path):
|
|||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": [
|
||||
{"type": "text", "text": "Hello "},
|
||||
{"type": "text", "text": "world"},
|
||||
{"type": "image_url", "image_url": "ignored"},
|
||||
]
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": [
|
||||
{"type": "text", "text": "Hello "},
|
||||
{"type": "text", "text": "world"},
|
||||
{"type": "image_url", "image_url": "ignored"},
|
||||
]
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
||||
},
|
||||
)
|
||||
)
|
||||
provider = OpenRouterPdfProvider(
|
||||
api_key="sk-or-test", base_url=_BASE, model="x/y"
|
||||
)
|
||||
provider = OpenRouterPdfProvider(api_key="sk-or-test", base_url=_BASE, model="x/y")
|
||||
response = await provider.complete(prompt="hi", pdf_path=tiny_pdf)
|
||||
assert response.text == "Hello world"
|
||||
|
||||
|
|
|
|||
|
|
@ -65,13 +65,15 @@ class TestParser:
|
|||
interaction_id="abc",
|
||||
query="Who directed Inception?",
|
||||
answer="Christopher Nolan",
|
||||
pages=[{
|
||||
"page_name": "Inception (film)",
|
||||
"page_url": "https://en.wikipedia.org/wiki/Inception",
|
||||
"page_snippet": "snippet",
|
||||
"page_result": "<html>full html</html>",
|
||||
"page_last_modified": "2024-01-01",
|
||||
}],
|
||||
pages=[
|
||||
{
|
||||
"page_name": "Inception (film)",
|
||||
"page_url": "https://en.wikipedia.org/wiki/Inception",
|
||||
"page_snippet": "snippet",
|
||||
"page_result": "<html>full html</html>",
|
||||
"page_last_modified": "2024-01-01",
|
||||
}
|
||||
],
|
||||
),
|
||||
]
|
||||
path = _make_jsonl_bz2(rows, tmp_path)
|
||||
|
|
@ -120,8 +122,7 @@ class TestParser:
|
|||
|
||||
def test_alt_answers_parsed(self, tmp_path: Path) -> None:
|
||||
rows = [
|
||||
_row(interaction_id="z", query="q?", answer="42",
|
||||
alt_ans=["forty-two", "42.0"]),
|
||||
_row(interaction_id="z", query="q?", answer="42", alt_ans=["forty-two", "42.0"]),
|
||||
]
|
||||
path = _make_jsonl_bz2(rows, tmp_path)
|
||||
parsed = iter_questions(path)
|
||||
|
|
@ -143,22 +144,32 @@ class TestParser:
|
|||
class TestPageHash:
|
||||
def test_url_hash_stable(self) -> None:
|
||||
a = CragPage(
|
||||
page_name="A", page_url="https://x.test/p?q=1",
|
||||
page_snippet="", page_html="<html/>",
|
||||
page_name="A",
|
||||
page_url="https://x.test/p?q=1",
|
||||
page_snippet="",
|
||||
page_html="<html/>",
|
||||
)
|
||||
b = CragPage(
|
||||
page_name="B", page_url="https://x.test/p?q=1",
|
||||
page_snippet="", page_html="<html/>",
|
||||
page_name="B",
|
||||
page_url="https://x.test/p?q=1",
|
||||
page_snippet="",
|
||||
page_html="<html/>",
|
||||
)
|
||||
assert a.url_hash == b.url_hash
|
||||
assert len(a.url_hash) == 12
|
||||
|
||||
def test_url_hash_unique(self) -> None:
|
||||
a = CragPage(
|
||||
page_name="A", page_url="https://x.test/a", page_snippet="", page_html="<html/>",
|
||||
page_name="A",
|
||||
page_url="https://x.test/a",
|
||||
page_snippet="",
|
||||
page_html="<html/>",
|
||||
)
|
||||
b = CragPage(
|
||||
page_name="B", page_url="https://x.test/b", page_snippet="", page_html="<html/>",
|
||||
page_name="B",
|
||||
page_url="https://x.test/b",
|
||||
page_snippet="",
|
||||
page_html="<html/>",
|
||||
)
|
||||
assert a.url_hash != b.url_hash
|
||||
|
||||
|
|
@ -174,21 +185,23 @@ class TestStratifiedSample:
|
|||
(5, "sports", "multi-hop"),
|
||||
):
|
||||
for _ in range(n):
|
||||
out.append(CragQuestion(
|
||||
qid=f"C{idx:05d}",
|
||||
interaction_id=f"i{idx}",
|
||||
query_time="2024-01-01",
|
||||
query=f"q{idx}?",
|
||||
gold_answer="a",
|
||||
alt_answers=[],
|
||||
domain=domain,
|
||||
question_type=qtype,
|
||||
static_or_dynamic="static",
|
||||
popularity="head",
|
||||
split=0,
|
||||
raw_index=idx,
|
||||
pages=[],
|
||||
))
|
||||
out.append(
|
||||
CragQuestion(
|
||||
qid=f"C{idx:05d}",
|
||||
interaction_id=f"i{idx}",
|
||||
query_time="2024-01-01",
|
||||
query=f"q{idx}?",
|
||||
gold_answer="a",
|
||||
alt_answers=[],
|
||||
domain=domain,
|
||||
question_type=qtype,
|
||||
static_or_dynamic="static",
|
||||
popularity="head",
|
||||
split=0,
|
||||
raw_index=idx,
|
||||
pages=[],
|
||||
)
|
||||
)
|
||||
idx += 1
|
||||
return out
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,9 @@ class TestGradeDeterministicHappyPath:
|
|||
class TestGradeDeterministicRefusal:
|
||||
def test_idk_maps_to_missing(self) -> None:
|
||||
result = grade_deterministic(
|
||||
pred="I don't know.", gold="Tim Cook", question_type="simple",
|
||||
pred="I don't know.",
|
||||
gold="Tim Cook",
|
||||
question_type="simple",
|
||||
)
|
||||
assert result.grade == "missing"
|
||||
assert result.score == 0
|
||||
|
|
@ -225,8 +227,11 @@ class TestGradeDeterministicLexicalMiss:
|
|||
class TestGradeResultShape:
|
||||
def test_to_dict_round_trip(self) -> None:
|
||||
result = CragGradeResult(
|
||||
grade="correct", score=1, method="exact",
|
||||
normalised_pred="x", normalised_gold="x",
|
||||
grade="correct",
|
||||
score=1,
|
||||
method="exact",
|
||||
normalised_pred="x",
|
||||
normalised_gold="x",
|
||||
)
|
||||
d = result.to_dict()
|
||||
assert d["grade"] == "correct"
|
||||
|
|
|
|||
|
|
@ -112,7 +112,9 @@ class TestFallbackStripper:
|
|||
</body></html>
|
||||
"""
|
||||
result = extract_main_content(
|
||||
html, url="https://x.test/", page_name="Title",
|
||||
html,
|
||||
url="https://x.test/",
|
||||
page_name="Title",
|
||||
)
|
||||
assert result.ok
|
||||
assert "content one" in result.text
|
||||
|
|
|
|||
|
|
@ -63,14 +63,22 @@ class TestCacheFilename:
|
|||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_fetch_success_writes_markdown(tmp_path: Path) -> None:
|
||||
respx.get(WIKI_API).mock(return_value=httpx.Response(
|
||||
200,
|
||||
json={"query": {"pages": [{
|
||||
"pageid": 1,
|
||||
"title": "James Buchanan",
|
||||
"extract": "James Buchanan was the 15th president of the United States.",
|
||||
}]}},
|
||||
))
|
||||
respx.get(WIKI_API).mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"query": {
|
||||
"pages": [
|
||||
{
|
||||
"pageid": 1,
|
||||
"title": "James Buchanan",
|
||||
"extract": "James Buchanan was the 15th president of the United States.",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
fetcher = WikiFetcher(cache_dir=tmp_path, rate_limit_rps=100) # disable throttle
|
||||
article = await fetcher.fetch("https://en.wikipedia.org/wiki/James_Buchanan")
|
||||
assert article is not None
|
||||
|
|
@ -83,13 +91,21 @@ async def test_fetch_success_writes_markdown(tmp_path: Path) -> None:
|
|||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_fetch_missing_page_returns_none(tmp_path: Path) -> None:
|
||||
respx.get(WIKI_API).mock(return_value=httpx.Response(
|
||||
200,
|
||||
json={"query": {"pages": [{
|
||||
"title": "DoesNotExist",
|
||||
"missing": True,
|
||||
}]}},
|
||||
))
|
||||
respx.get(WIKI_API).mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"query": {
|
||||
"pages": [
|
||||
{
|
||||
"title": "DoesNotExist",
|
||||
"missing": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
fetcher = WikiFetcher(cache_dir=tmp_path, rate_limit_rps=100)
|
||||
article = await fetcher.fetch("https://en.wikipedia.org/wiki/DoesNotExist")
|
||||
assert article is None
|
||||
|
|
|
|||
|
|
@ -99,7 +99,9 @@ class TestListFormat:
|
|||
assert 0.0 < r.f1 < 1.0
|
||||
|
||||
def test_extra_items_lower_precision(self) -> None:
|
||||
r = grade(pred="apple, banana, cherry, date", gold="apple, banana, cherry", answer_format="List")
|
||||
r = grade(
|
||||
pred="apple, banana, cherry, date", gold="apple, banana, cherry", answer_format="List"
|
||||
)
|
||||
assert 0.0 < r.f1 < 1.0
|
||||
# Recall=1, precision=3/4 → F1 ~= 0.857
|
||||
assert r.f1 == pytest.approx(2 * (3 / 4) * 1 / (3 / 4 + 1), rel=1e-3)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue