Files
Hy-Memory/tests/coding/_helpers.py
T
2026-10-16 16:26:27 +08:00

128 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""测试辅助类(不被 pytest 收集为 test)"""
import hashlib
import json
from typing import Any, Dict, List, Optional, Union
class MockLLMProvider:
"""
用预先准备好的回复序列响应 complete() / complete_messages()。每次 pop 一个;用完后重复最后一个。
response 项支持两种形式(向后兼容):
- str : 纯文本回复(老用法)
- Dict[str, Any] : 结构化回复,可含 tool_calls,用于测试 tool calling agent
形如 {"content": "...", "tool_calls": [{"id": ..., "function": {...}}]}
"""
def __init__(self, responses):
if isinstance(responses, (str, dict)):
responses = [responses]
self._responses: List[Union[str, Dict[str, Any]]] = list(responses)
self.calls: List[Dict[str, Any]] = []
self._idx = 0
# 兼容 coding 模块对 _llm_config.temperature 的 fallback 探测
self._llm_config = type("_FakeCfg", (), {"temperature": 0.1, "model": "mock"})()
def _next(self) -> Union[str, Dict[str, Any]]:
if self._idx < len(self._responses):
r = self._responses[self._idx]
self._idx += 1
else:
r = self._responses[-1]
return r
def _build_response(self, raw: Union[str, Dict[str, Any]]):
from hy_memory.agent.llm_provider import LLMResponse
if isinstance(raw, str):
return LLMResponse(content=raw, tokens_used=10)
# Dict
content = raw.get("content", "") or ""
tool_calls = raw.get("tool_calls")
# tool_calls 内的 function.arguments 既允许 dict 也允许 JSON string;统一成 OpenAI 风格 string
if tool_calls:
normalized = []
for tc in tool_calls:
fn = tc.get("function", {})
args = fn.get("arguments")
if isinstance(args, dict):
args = json.dumps(args, ensure_ascii=False)
normalized.append({
"id": tc.get("id", "") or "",
"type": tc.get("type", "function"),
"function": {
"name": fn.get("name", ""),
"arguments": args or "{}",
},
})
tool_calls = normalized
return LLMResponse(content=content, tokens_used=10, tool_calls=tool_calls)
async def complete(self, prompt, max_tokens=500, temperature=0.7, tools=None, tool_choice=None, **kwargs):
self.calls.append({
"method": "complete",
"prompt": prompt,
"max_tokens": max_tokens,
"temperature": temperature,
"tools": tools,
"tool_choice": tool_choice,
})
return self._build_response(self._next())
async def complete_messages(self, messages, max_tokens=500, temperature=0.7,
tools=None, tool_choice=None, **kwargs):
self.calls.append({
"method": "complete_messages",
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
"tools": tools,
"tool_choice": tool_choice,
})
return self._build_response(self._next())
class FailingLLMProvider:
"""每次 complete / complete_messages 都 raise,用于 fail-safe 路径测试"""
def __init__(self):
self.calls = 0
self._llm_config = type("_FakeCfg", (), {"temperature": 0.1, "model": "mock"})()
async def complete(self, **kwargs):
self.calls += 1
raise RuntimeError("mock LLM failure")
async def complete_messages(self, **kwargs):
self.calls += 1
raise RuntimeError("mock LLM failure")
class MockEmbedService:
async def embed(self, text):
return self._hash_to_vec(text)
async def embed_batch(self, texts):
return [self._hash_to_vec(t) for t in texts]
async def embed_with_meta(self, text, use_cache: bool = True):
return self._hash_to_vec(text), {"cache_hit": False, "mock": True}
async def embed_batch_with_meta(self, texts, use_cache: bool = True):
vecs = [self._hash_to_vec(t) for t in texts]
metas = [{"cache_hit": False, "mock": True} for _ in texts]
return vecs, metas
async def embed_queued(self, text, use_cache: bool = True):
return self._hash_to_vec(text)
async def embed_queued_with_meta(self, text, use_cache: bool = True):
return self._hash_to_vec(text), {"cache_hit": False, "mock": True}
@staticmethod
def _hash_to_vec(t: str, dims: int = 1536):
h = hashlib.md5(t.encode()).digest() # 16 字节
v = [b / 255.0 for b in h] * (dims // 16)
return v[:dims]