test_legacy_active_inputs.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. from __future__ import annotations
  2. import json
  3. from types import SimpleNamespace
  4. import httpx
  5. import numpy as np
  6. import pytest
  7. from script_build_host.adapters.legacy_retrieval import (
  8. LegacyExternalRetrievalAdapter,
  9. LegacyLlmKnowledgeRetrievalAdapter,
  10. LegacyOpenRouterClient,
  11. LegacyVectorDecodeRetrievalAdapter,
  12. )
  13. from script_build_host.adapters.retrieval import SafeHttpClient
  14. from script_build_host.infrastructure.outbound import OutboundPolicy
  15. async def _public_resolver(_: str, __: int) -> tuple[str, ...]:
  16. return ("93.184.216.34",)
  17. def _decode_fixture(tmp_path):
  18. index = tmp_path / "index"
  19. raw = tmp_path / "raw" / "acct"
  20. index.mkdir()
  21. raw.mkdir(parents=True)
  22. (index / "manifest.json").write_text(
  23. json.dumps({"model": "embed", "dimension": 2}), encoding="utf-8"
  24. )
  25. chunks = [
  26. {
  27. "chunk_id": "p1|1",
  28. "post_id": "p1",
  29. "account": "acct",
  30. "json_relpath": "script_decode_raw_data/acct/p1.json",
  31. "column": "主题",
  32. "sub_field": "维度名",
  33. },
  34. {
  35. "chunk_id": "p2|1",
  36. "post_id": "p2",
  37. "account": "other",
  38. "json_relpath": "script_decode_raw_data/acct/p1.json",
  39. "column": "形式",
  40. "sub_field": "维度值",
  41. },
  42. ]
  43. (index / "meta.jsonl").write_text(
  44. "".join(json.dumps(item, ensure_ascii=False) + "\n" for item in chunks),
  45. encoding="utf-8",
  46. )
  47. np.save(index / "embeddings.npy", np.asarray([[1, 0], [0, 1]], dtype=np.float16))
  48. (raw / "p1.json").write_text(
  49. json.dumps(
  50. {
  51. "段落列表": [
  52. {
  53. "id": "段落1",
  54. "名称": "开头",
  55. "主题": "主题正文",
  56. "描述": "完整描述",
  57. "主题原子点": [{"维度类型": "主维度", "维度名": "对象", "维度值": "测试"}],
  58. }
  59. ]
  60. },
  61. ensure_ascii=False,
  62. ),
  63. encoding="utf-8",
  64. )
  65. return index, tmp_path / "raw"
  66. @pytest.mark.asyncio
  67. async def test_http_is_permitted_only_for_the_separate_legacy_allowlist() -> None:
  68. policy = OutboundPolicy(
  69. allowed_hosts=frozenset({"legacy.example"}),
  70. allowed_ports=frozenset({80}),
  71. allowed_http_hosts=frozenset({"legacy.example"}),
  72. resolver=_public_resolver,
  73. )
  74. assert await policy.validate_url("http://legacy.example/input") == (
  75. "http://legacy.example/input"
  76. )
  77. denied = OutboundPolicy(
  78. allowed_hosts=frozenset({"legacy.example"}),
  79. allowed_ports=frozenset({80}),
  80. resolver=_public_resolver,
  81. )
  82. with pytest.raises(Exception, match="HTTPS"):
  83. await denied.validate_url("http://legacy.example/input")
  84. @pytest.mark.asyncio
  85. async def test_legacy_decode_supports_account_only_and_semantic_vector_search(tmp_path) -> None:
  86. index, raw = _decode_fixture(tmp_path)
  87. def handler(request: httpx.Request) -> httpx.Response:
  88. assert request.headers["authorization"] == "Bearer secret"
  89. return httpx.Response(200, json={"data": [{"index": 0, "embedding": [1, 0]}]})
  90. async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
  91. safe = SafeHttpClient(
  92. client,
  93. OutboundPolicy(frozenset({"openrouter.example"}), resolver=_public_resolver),
  94. )
  95. openrouter = LegacyOpenRouterClient(
  96. safe,
  97. api_key="secret",
  98. embedding_endpoint="https://openrouter.example/embeddings",
  99. chat_endpoint="https://openrouter.example/chat",
  100. embedding_model="embed",
  101. embedding_dimension=2,
  102. )
  103. adapter = LegacyVectorDecodeRetrievalAdapter(
  104. index_root=index, raw_root=raw, openrouter=openrouter
  105. )
  106. account_result = await adapter.retrieve(
  107. query={
  108. "return_field": "主脉络",
  109. "keyword": "",
  110. "account_name": "acct",
  111. "match_fields": {},
  112. "top_k": 3,
  113. },
  114. snapshot=SimpleNamespace(),
  115. )
  116. semantic_result = await adapter.retrieve(
  117. query={
  118. "return_field": "主题",
  119. "keyword": "测试",
  120. "account_name": None,
  121. "match_fields": {"主题": "维度名"},
  122. "top_k": 3,
  123. },
  124. snapshot=SimpleNamespace(),
  125. )
  126. assert account_result.source_refs == ("decode:p1",)
  127. assert "主维度" in account_result.summary
  128. assert semantic_result.metadata["scores"] == [1.0]
  129. assert "主题正文" in semantic_result.summary
  130. @pytest.mark.asyncio
  131. async def test_legacy_knowledge_uses_full_llm_selection_contract(tmp_path) -> None:
  132. source = tmp_path / "knowledge.json"
  133. source.write_text(
  134. json.dumps(
  135. [
  136. {
  137. "id": 7,
  138. "title": "结构",
  139. "purpose": "搭建结构",
  140. "steps": [
  141. {
  142. "intent": "开头",
  143. "directive": "先给结果",
  144. "outputs": [{"value": "钩子"}],
  145. }
  146. ],
  147. }
  148. ],
  149. ensure_ascii=False,
  150. ),
  151. encoding="utf-8",
  152. )
  153. bodies = []
  154. def handler(request: httpx.Request) -> httpx.Response:
  155. bodies.append(json.loads(request.content))
  156. return httpx.Response(
  157. 200,
  158. json={"choices": [{"message": {"content": '{"ids":[7]}'}}]},
  159. )
  160. async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
  161. safe = SafeHttpClient(
  162. client,
  163. OutboundPolicy(frozenset({"openrouter.example"}), resolver=_public_resolver),
  164. )
  165. openrouter = LegacyOpenRouterClient(
  166. safe,
  167. api_key="secret",
  168. embedding_endpoint="https://openrouter.example/embeddings",
  169. chat_endpoint="https://openrouter.example/chat",
  170. embedding_model="embed",
  171. embedding_dimension=2,
  172. )
  173. result = await LegacyLlmKnowledgeRetrievalAdapter(
  174. source, openrouter=openrouter, model="knowledge-model"
  175. ).retrieve(query={"keyword": "结构", "max_count": 3}, snapshot=SimpleNamespace())
  176. assert bodies[0]["model"] == "knowledge-model"
  177. assert "先给结果" in bodies[0]["messages"][1]["content"]
  178. assert "钩子" in result.summary
  179. @pytest.mark.asyncio
  180. async def test_legacy_external_reproduces_xhs_detail_and_zhihu_shapes() -> None:
  181. requests: list[tuple[str, dict[str, object]]] = []
  182. def handler(request: httpx.Request) -> httpx.Response:
  183. body = json.loads(request.content)
  184. requests.append((request.url.path, body))
  185. if request.url.path == "/xhs/search":
  186. return httpx.Response(
  187. 200,
  188. json={"code": 0, "data": {"data": [{"id": "note-1"}], "has_more": False}},
  189. )
  190. if request.url.path == "/xhs/detail":
  191. return httpx.Response(
  192. 200,
  193. json={
  194. "code": 0,
  195. "data": {
  196. "data": {
  197. "channel_content_id": "note-1",
  198. "title": "标题",
  199. "body_text": "正文",
  200. "like_count": "12",
  201. "publish_timestamp": 100,
  202. "image_url_list": [
  203. {"image_url": "https://image.example/a.jpg"},
  204. {"image_url": "https://image.example/a.jpg"},
  205. ],
  206. }
  207. },
  208. },
  209. )
  210. return httpx.Response(200, json={"code": 0, "data": [{"id": "zhihu-1"}]})
  211. async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
  212. safe = SafeHttpClient(
  213. client,
  214. OutboundPolicy(frozenset({"legacy.example"}), resolver=_public_resolver),
  215. )
  216. adapter = LegacyExternalRetrievalAdapter(
  217. xhs_search_endpoint="https://legacy.example/xhs/search",
  218. xhs_detail_endpoint="https://legacy.example/xhs/detail",
  219. zhihu_search_endpoint="https://legacy.example/zhihu",
  220. http=safe,
  221. )
  222. xhs = await adapter.retrieve(
  223. query={"keyword": "测试", "platform_channel": "xhs", "max_count": 5},
  224. snapshot=SimpleNamespace(),
  225. )
  226. zhihu = await adapter.retrieve(
  227. query={"keyword": "测试", "platform_channel": "zhihu", "max_count": 5},
  228. snapshot=SimpleNamespace(),
  229. )
  230. xhs_rows = json.loads(xhs.summary)
  231. assert xhs_rows[0]["publish_timestamp"] == 100_000
  232. assert xhs_rows[0]["images"] == ["https://image.example/a.jpg"]
  233. assert xhs_rows[0]["images_text_escape"] == [
  234. {"image_url": "https://image.example/a.jpg", "text": ""}
  235. ]
  236. assert zhihu.source_refs == ("external:zhihu-1",)
  237. assert requests[0][1]["content_type"] == "图文"