| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- from __future__ import annotations
- import json
- from types import SimpleNamespace
- import httpx
- import numpy as np
- import pytest
- from script_build_host.adapters.legacy_retrieval import (
- LegacyExternalRetrievalAdapter,
- LegacyLlmKnowledgeRetrievalAdapter,
- LegacyOpenRouterClient,
- LegacyVectorDecodeRetrievalAdapter,
- OpenAICompatibleEmbeddingClient,
- )
- from script_build_host.adapters.retrieval import SafeHttpClient
- from script_build_host.infrastructure.outbound import OutboundPolicy
- async def _public_resolver(_: str, __: int) -> tuple[str, ...]:
- return ("93.184.216.34",)
- def _decode_fixture(tmp_path):
- index = tmp_path / "index"
- raw = tmp_path / "raw" / "acct"
- index.mkdir()
- raw.mkdir(parents=True)
- (index / "manifest.json").write_text(
- json.dumps({"model": "embed", "dimension": 2}), encoding="utf-8"
- )
- chunks = [
- {
- "chunk_id": "p1|1",
- "post_id": "p1",
- "account": "acct",
- "json_relpath": "script_decode_raw_data/acct/p1.json",
- "column": "主题",
- "sub_field": "维度名",
- },
- {
- "chunk_id": "p2|1",
- "post_id": "p2",
- "account": "other",
- "json_relpath": "script_decode_raw_data/acct/p1.json",
- "column": "形式",
- "sub_field": "维度值",
- },
- ]
- (index / "meta.jsonl").write_text(
- "".join(json.dumps(item, ensure_ascii=False) + "\n" for item in chunks),
- encoding="utf-8",
- )
- np.save(index / "embeddings.npy", np.asarray([[1, 0], [0, 1]], dtype=np.float16))
- (raw / "p1.json").write_text(
- json.dumps(
- {
- "段落列表": [
- {
- "id": "段落1",
- "名称": "开头",
- "主题": "主题正文",
- "描述": "完整描述",
- "主题原子点": [{"维度类型": "主维度", "维度名": "对象", "维度值": "测试"}],
- }
- ]
- },
- ensure_ascii=False,
- ),
- encoding="utf-8",
- )
- return index, tmp_path / "raw"
- @pytest.mark.asyncio
- async def test_http_is_permitted_only_for_the_separate_legacy_allowlist() -> None:
- policy = OutboundPolicy(
- allowed_hosts=frozenset({"legacy.example"}),
- allowed_ports=frozenset({80}),
- allowed_http_hosts=frozenset({"legacy.example"}),
- resolver=_public_resolver,
- )
- assert await policy.validate_url("http://legacy.example/input") == (
- "http://legacy.example/input"
- )
- denied = OutboundPolicy(
- allowed_hosts=frozenset({"legacy.example"}),
- allowed_ports=frozenset({80}),
- resolver=_public_resolver,
- )
- with pytest.raises(Exception, match="HTTPS"):
- await denied.validate_url("http://legacy.example/input")
- @pytest.mark.asyncio
- async def test_legacy_decode_supports_account_only_and_semantic_vector_search(tmp_path) -> None:
- index, raw = _decode_fixture(tmp_path)
- def handler(request: httpx.Request) -> httpx.Response:
- assert request.headers["authorization"] == "Bearer secret"
- return httpx.Response(200, json={"data": [{"index": 0, "embedding": [1, 0]}]})
- async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
- safe = SafeHttpClient(
- client,
- OutboundPolicy(frozenset({"openrouter.example"}), resolver=_public_resolver),
- )
- embedding_client = OpenAICompatibleEmbeddingClient(
- safe,
- api_key="secret",
- embedding_endpoint="https://openrouter.example/embeddings",
- embedding_model="embed",
- embedding_dimension=2,
- )
- adapter = LegacyVectorDecodeRetrievalAdapter(
- index_root=index, raw_root=raw, embedding_client=embedding_client
- )
- account_result = await adapter.retrieve(
- query={
- "return_field": "主脉络",
- "keyword": "",
- "account_name": "acct",
- "match_fields": {},
- "top_k": 3,
- },
- snapshot=SimpleNamespace(),
- )
- semantic_result = await adapter.retrieve(
- query={
- "return_field": "主题",
- "keyword": "测试",
- "account_name": None,
- "match_fields": {"主题": "维度名"},
- "top_k": 3,
- },
- snapshot=SimpleNamespace(),
- )
- assert account_result.source_refs == ("decode:p1",)
- assert "主维度" in account_result.summary
- assert semantic_result.metadata["scores"] == [1.0]
- assert "主题正文" in semantic_result.summary
- @pytest.mark.asyncio
- async def test_legacy_decode_rejects_query_model_that_differs_from_index(tmp_path) -> None:
- index, raw = _decode_fixture(tmp_path)
- async with httpx.AsyncClient(
- transport=httpx.MockTransport(lambda _: httpx.Response(500))
- ) as client:
- safe = SafeHttpClient(
- client,
- OutboundPolicy(frozenset({"embedding.example"}), resolver=_public_resolver),
- )
- embedding_client = OpenAICompatibleEmbeddingClient(
- safe,
- api_key="secret",
- embedding_endpoint="https://embedding.example/embeddings",
- embedding_model="different-model",
- embedding_dimension=2,
- )
- adapter = LegacyVectorDecodeRetrievalAdapter(
- index_root=index,
- raw_root=raw,
- embedding_client=embedding_client,
- )
- with pytest.raises(Exception, match="model does not match"):
- await adapter.retrieve(
- query={
- "return_field": "主题",
- "keyword": "测试",
- "account_name": None,
- "match_fields": {},
- "top_k": 3,
- },
- snapshot=SimpleNamespace(),
- )
- @pytest.mark.asyncio
- async def test_legacy_knowledge_uses_full_llm_selection_contract(tmp_path) -> None:
- source = tmp_path / "knowledge.json"
- source.write_text(
- json.dumps(
- [
- {
- "id": 7,
- "title": "结构",
- "purpose": "搭建结构",
- "steps": [
- {
- "intent": "开头",
- "directive": "先给结果",
- "outputs": [{"value": "钩子"}],
- }
- ],
- }
- ],
- ensure_ascii=False,
- ),
- encoding="utf-8",
- )
- bodies = []
- def handler(request: httpx.Request) -> httpx.Response:
- bodies.append(json.loads(request.content))
- return httpx.Response(
- 200,
- json={"choices": [{"message": {"content": '{"ids":[7]}'}}]},
- )
- async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
- safe = SafeHttpClient(
- client,
- OutboundPolicy(frozenset({"openrouter.example"}), resolver=_public_resolver),
- )
- openrouter = LegacyOpenRouterClient(
- safe,
- api_key="secret",
- chat_endpoint="https://openrouter.example/chat",
- )
- result = await LegacyLlmKnowledgeRetrievalAdapter(
- source, openrouter=openrouter, model="knowledge-model"
- ).retrieve(query={"keyword": "结构", "max_count": 3}, snapshot=SimpleNamespace())
- assert bodies[0]["model"] == "knowledge-model"
- assert "先给结果" in bodies[0]["messages"][1]["content"]
- assert "钩子" in result.summary
- @pytest.mark.asyncio
- async def test_legacy_external_reproduces_xhs_detail_and_zhihu_shapes() -> None:
- requests: list[tuple[str, dict[str, object]]] = []
- def handler(request: httpx.Request) -> httpx.Response:
- body = json.loads(request.content)
- requests.append((request.url.path, body))
- if request.url.path == "/xhs/search":
- return httpx.Response(
- 200,
- json={"code": 0, "data": {"data": [{"id": "note-1"}], "has_more": False}},
- )
- if request.url.path == "/xhs/detail":
- return httpx.Response(
- 200,
- json={
- "code": 0,
- "data": {
- "data": {
- "channel_content_id": "note-1",
- "title": "标题",
- "body_text": "正文",
- "like_count": "12",
- "publish_timestamp": 100,
- "image_url_list": [
- {"image_url": "https://image.example/a.jpg"},
- {"image_url": "https://image.example/a.jpg"},
- ],
- }
- },
- },
- )
- return httpx.Response(200, json={"code": 0, "data": [{"id": "zhihu-1"}]})
- async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
- safe = SafeHttpClient(
- client,
- OutboundPolicy(frozenset({"legacy.example"}), resolver=_public_resolver),
- )
- adapter = LegacyExternalRetrievalAdapter(
- xhs_search_endpoint="https://legacy.example/xhs/search",
- xhs_detail_endpoint="https://legacy.example/xhs/detail",
- zhihu_search_endpoint="https://legacy.example/zhihu",
- http=safe,
- )
- xhs = await adapter.retrieve(
- query={"keyword": "测试", "platform_channel": "xhs", "max_count": 5},
- snapshot=SimpleNamespace(),
- )
- zhihu = await adapter.retrieve(
- query={"keyword": "测试", "platform_channel": "zhihu", "max_count": 5},
- snapshot=SimpleNamespace(),
- )
- xhs_rows = json.loads(xhs.summary)
- assert xhs_rows[0]["publish_timestamp"] == 100_000
- assert xhs_rows[0]["images"] == ["https://image.example/a.jpg"]
- assert xhs_rows[0]["images_text_escape"] == [
- {"image_url": "https://image.example/a.jpg", "text": ""}
- ]
- assert zhihu.source_refs == ("external:zhihu-1",)
- assert requests[0][1]["content_type"] == "图文"
|