fakes.py 4.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. """LLM test double: production has no offline route; tests avoid network calls."""
  2. from __future__ import annotations
  3. import json
  4. import re
  5. from dataclasses import dataclass
  6. from typing import Iterable
  7. @dataclass
  8. class _Status:
  9. reason: str = "test Hello-Agents runtime"
  10. enabled: bool = True
  11. class FakeRuntime:
  12. """Contract-level stand-in for HelloAgentsRuntime used only by unit tests."""
  13. def __init__(self) -> None:
  14. self.status = _Status()
  15. self.registered: tuple[str, ...] = ()
  16. def register_tool_functions(self, functions) -> None:
  17. self.registered = tuple(functions)
  18. def status_dict(self) -> dict:
  19. return {
  20. "available": True,
  21. "enabled": True,
  22. "reason": self.status.reason,
  23. "registry_name": "Fake ToolRegistry",
  24. "paradigms": ["ReActAgent", "PlanSolveAgent", "ReflectionAgent"],
  25. "registered_tools": list(self.registered),
  26. }
  27. def classify_uncertain(self, merchant: str, note: str, allowed_categories: list[str]) -> str:
  28. return "其他" if "其他" in allowed_categories else allowed_categories[0]
  29. @staticmethod
  30. def _quest_json(prompt: str) -> str:
  31. """Mirror only the strict JSON protocol, not production Quest logic."""
  32. marker = "SIGNAL_CATALOG_JSON:\n"
  33. catalog: list[dict] = []
  34. if marker in prompt:
  35. raw = prompt.split(marker, 1)[1]
  36. decoder = json.JSONDecoder()
  37. try:
  38. catalog, _ = decoder.raw_decode(raw.lstrip())
  39. except json.JSONDecodeError:
  40. catalog = []
  41. names = {
  42. "late_night": ("夜航冷静结界", "给深夜冲动留一段缓冲,不必用意志硬扛。", "先辨认触发场景,再为自己准备替代选项。"),
  43. "frequent_small": ("零钱能量巡逻", "把细碎消费当作线索,找回自己选择的节奏。", "出门前先想好今天最想守住的体验。"),
  44. "flexible_budget": ("弹性钱包护盾", "体验额度依然保留,只是让目标也拥有位置。", "付款前停一停,确认这次消费是否真的值得。"),
  45. "subscriptions": ("订阅遗迹寻宝", "把持续扣费翻出来,留下真正陪伴你的服务。", "从最近使用感受开始,逐项做一个保留决定。"),
  46. "weekend": ("周末钱包护盾", "周末可以尽兴,也可以留下一点可控的边界。", "安排活动前先选定最想投入的一件事。"),
  47. "payday": ("发薪冷静回合", "到账后的兴奋值得被看见,也值得多一点缓冲。", "先把想买的东西记下,稍后再决定是否结算。"),
  48. "learning_followthrough": ("学习战利品回访", "让学习消费继续产生陪伴感,而不只是一次付款。", "选一个最容易开始的学习入口,写下下次打开它的时机。"),
  49. "goal_transfer": ("目标补给路线", "让本月结余有一个温柔去处,持续靠近你的愿望。", "先确认最想推进的目标,再写下这次结余的安排。"),
  50. "balance": ("镜像决策日志", "消费没有标准答案,记录会帮你看见自己的偏好。", "挑一笔最近消费,写下它带来的真实感受。"),
  51. }
  52. quests = []
  53. for signal in catalog:
  54. signal_id = signal.get("signal_id")
  55. if signal_id in names:
  56. title, narrative, action_hint = names[signal_id]
  57. quests.append({"signal_id": signal_id, "title": title, "narrative": narrative, "action_hint": action_hint})
  58. return json.dumps({"quests": quests[:5]}, ensure_ascii=False)
  59. def explain(self, prompt: str, mode: str = "simple", evidence: Iterable[str] = (), memory: Iterable[str] = ()) -> str:
  60. if "SIGNAL_CATALOG_JSON:" in prompt or "Quest JSON" in prompt:
  61. return self._quest_json(prompt)
  62. if mode == "reflection":
  63. return "已根据验证的账单、预算和目标证据完成本轮反思;下一步选择一个低摩擦任务即可。"
  64. if mode == "plan":
  65. return "先阅读已验证的消费证据,再挑选一个能在本周完成的小行动。"
  66. return "这是一段基于已验证消费证据生成的 AI 引导文案。"
  67. def generate_quest_candidates(self, prompt: str) -> str:
  68. return self._quest_json(prompt)
  69. def generate_markdown(self, report_payload: str) -> str:
  70. return "# MoneyMirrorAgent 月度报告\n\n这是由测试 LLM 生成的 Markdown 报告。\n"
  71. def stream_user_guidance(self, question: str, report_payload: str, history):
  72. yield "🪞 先从一个小行动开始:"
  73. yield "本周记录一次触发消费的场景,然后告诉我你的发现。"