test_application_runtime.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import unittest
  2. from pydantic import ValidationError
  3. from cyber_agent.application import (
  4. AgentApplication,
  5. AgentRole,
  6. ApplicationRegistry,
  7. ApplicationServices,
  8. ProviderRef,
  9. RoleRunLimits,
  10. RunLimits,
  11. SkillSpec,
  12. ToolSet,
  13. ToolSpec,
  14. )
  15. from cyber_agent.tools.registry import ToolRegistry
  16. def application(*, prompt="write carefully", provider_version="1", limit=20):
  17. return AgentApplication(
  18. application_id="creative.reference",
  19. application_version="0.1.0",
  20. root_role="writer",
  21. roles=(AgentRole(
  22. role_id="writer",
  23. model="fake-model",
  24. system_prompt=prompt,
  25. skills=(SkillSpec(
  26. name="evidence-writing",
  27. version="1",
  28. content="Use checked evidence.",
  29. ),),
  30. tool_sets=("writing",),
  31. allowed_child_roles=("writer",),
  32. run_limits=RoleRunLimits(max_iterations=limit),
  33. ),),
  34. tool_sets=(ToolSet(
  35. tool_set_id="writing",
  36. tools=(ToolSpec(
  37. tool_name="save",
  38. implementation_version="1",
  39. side_effect_class="write",
  40. idempotent=True,
  41. ),),
  42. ),),
  43. context_provider_ref=ProviderRef(
  44. provider_id="context",
  45. provider_version=provider_version,
  46. ),
  47. run_limits=RunLimits(max_iterations=30),
  48. )
  49. def registry(output):
  50. tools = ToolRegistry()
  51. async def save(value: str):
  52. return {"application": output, "value": value}
  53. tools.register(save, groups=["application"])
  54. return tools
  55. class ApplicationDeclarationTest(unittest.TestCase):
  56. def test_role_graph_and_tool_set_references_fail_closed(self):
  57. with self.assertRaisesRegex(ValidationError, "unknown child roles"):
  58. AgentApplication(
  59. application_id="broken",
  60. application_version="1",
  61. root_role="root",
  62. roles=(AgentRole(
  63. role_id="root",
  64. model="fake",
  65. system_prompt="root",
  66. allowed_child_roles=("missing",),
  67. ),),
  68. )
  69. with self.assertRaisesRegex(ValidationError, "unknown tool sets"):
  70. AgentApplication(
  71. application_id="broken",
  72. application_version="1",
  73. root_role="root",
  74. roles=(AgentRole(
  75. role_id="root",
  76. model="fake",
  77. system_prompt="root",
  78. tool_sets=("missing",),
  79. ),),
  80. )
  81. def test_registry_freezes_private_tool_copy_and_resets_stats(self):
  82. source = registry("A")
  83. source._stats["save"].call_count = 7
  84. binding = ApplicationRegistry().register(
  85. application(),
  86. ApplicationServices(
  87. tool_registry=source,
  88. context_provider=object(),
  89. ),
  90. )
  91. self.assertTrue(binding.tool_registry.frozen)
  92. self.assertEqual(0, binding.tool_registry.get_stats("save")["save"]["call_count"])
  93. with self.assertRaisesRegex(RuntimeError, "frozen"):
  94. binding.tool_registry.register(lambda: None)
  95. self.assertFalse(source.frozen)
  96. def test_same_tool_name_is_isolated_between_application_bindings(self):
  97. app_a = application()
  98. app_b = app_a.model_copy(update={
  99. "application_id": "creative.other",
  100. "roles": (
  101. app_a.roles[0].model_copy(update={"system_prompt": "other prompt"}),
  102. ),
  103. })
  104. binding_a = ApplicationRegistry().register(
  105. app_a,
  106. ApplicationServices(
  107. tool_registry=registry("A"),
  108. context_provider=object(),
  109. ),
  110. )
  111. binding_b = ApplicationRegistry().register(
  112. app_b,
  113. ApplicationServices(
  114. tool_registry=registry("B"),
  115. context_provider=object(),
  116. ),
  117. )
  118. self.assertIsNot(binding_a.tool_registry, binding_b.tool_registry)
  119. self.assertNotEqual(
  120. binding_a.application_ref.config_hash,
  121. binding_b.application_ref.config_hash,
  122. )
  123. self.assertNotEqual(
  124. binding_a.role("writer").role_hash,
  125. binding_b.role("writer").role_hash,
  126. )
  127. def test_hash_covers_prompt_provider_limits_and_tool_schema(self):
  128. base = ApplicationRegistry().register(
  129. application(),
  130. ApplicationServices(
  131. tool_registry=registry("base"),
  132. context_provider=object(),
  133. ),
  134. ).application_ref.config_hash
  135. variants = [
  136. application(prompt="changed prompt"),
  137. application(provider_version="2"),
  138. application(limit=10),
  139. ]
  140. hashes = []
  141. for item in variants:
  142. hashes.append(ApplicationRegistry().register(
  143. item,
  144. ApplicationServices(
  145. tool_registry=registry("base"),
  146. context_provider=object(),
  147. ),
  148. ).application_ref.config_hash)
  149. self.assertTrue(all(item != base for item in hashes))
  150. changed_schema = registry("base")
  151. definition = changed_schema._tools["save"]
  152. definition["schema"] = {
  153. **definition["schema"],
  154. "function": {
  155. **definition["schema"]["function"],
  156. "description": "changed schema",
  157. },
  158. }
  159. schema_hash = ApplicationRegistry().register(
  160. application(),
  161. ApplicationServices(
  162. tool_registry=changed_schema,
  163. context_provider=object(),
  164. ),
  165. ).application_ref.config_hash
  166. self.assertNotEqual(base, schema_hash)
  167. def test_runtime_service_identity_is_not_part_of_config_hash(self):
  168. first = ApplicationRegistry().register(
  169. application(),
  170. ApplicationServices(
  171. tool_registry=registry("same"),
  172. context_provider=object(),
  173. ),
  174. )
  175. second = ApplicationRegistry().register(
  176. application(),
  177. ApplicationServices(
  178. tool_registry=registry("same"),
  179. context_provider=object(),
  180. ),
  181. )
  182. self.assertEqual(first.application_ref, second.application_ref)
  183. def test_missing_declared_provider_is_rejected(self):
  184. with self.assertRaisesRegex(ValueError, "context_provider"):
  185. ApplicationRegistry().register(
  186. application(),
  187. ApplicationServices(tool_registry=registry("A")),
  188. )
  189. if __name__ == "__main__":
  190. unittest.main()