planar 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. planar/.__init__.py.un~ +0 -0
  2. planar/._version.py.un~ +0 -0
  3. planar/.app.py.un~ +0 -0
  4. planar/.cli.py.un~ +0 -0
  5. planar/.config.py.un~ +0 -0
  6. planar/.context.py.un~ +0 -0
  7. planar/.db.py.un~ +0 -0
  8. planar/.di.py.un~ +0 -0
  9. planar/.engine.py.un~ +0 -0
  10. planar/.files.py.un~ +0 -0
  11. planar/.log_context.py.un~ +0 -0
  12. planar/.log_metadata.py.un~ +0 -0
  13. planar/.logging.py.un~ +0 -0
  14. planar/.object_registry.py.un~ +0 -0
  15. planar/.otel.py.un~ +0 -0
  16. planar/.server.py.un~ +0 -0
  17. planar/.session.py.un~ +0 -0
  18. planar/.sqlalchemy.py.un~ +0 -0
  19. planar/.task_local.py.un~ +0 -0
  20. planar/.test_app.py.un~ +0 -0
  21. planar/.test_config.py.un~ +0 -0
  22. planar/.test_object_config.py.un~ +0 -0
  23. planar/.test_sqlalchemy.py.un~ +0 -0
  24. planar/.test_utils.py.un~ +0 -0
  25. planar/.util.py.un~ +0 -0
  26. planar/.utils.py.un~ +0 -0
  27. planar/__init__.py +26 -0
  28. planar/_version.py +1 -0
  29. planar/ai/.__init__.py.un~ +0 -0
  30. planar/ai/._models.py.un~ +0 -0
  31. planar/ai/.agent.py.un~ +0 -0
  32. planar/ai/.agent_utils.py.un~ +0 -0
  33. planar/ai/.events.py.un~ +0 -0
  34. planar/ai/.files.py.un~ +0 -0
  35. planar/ai/.models.py.un~ +0 -0
  36. planar/ai/.providers.py.un~ +0 -0
  37. planar/ai/.pydantic_ai.py.un~ +0 -0
  38. planar/ai/.pydantic_ai_agent.py.un~ +0 -0
  39. planar/ai/.pydantic_ai_provider.py.un~ +0 -0
  40. planar/ai/.step.py.un~ +0 -0
  41. planar/ai/.test_agent.py.un~ +0 -0
  42. planar/ai/.test_agent_serialization.py.un~ +0 -0
  43. planar/ai/.test_providers.py.un~ +0 -0
  44. planar/ai/.utils.py.un~ +0 -0
  45. planar/ai/__init__.py +15 -0
  46. planar/ai/agent.py +457 -0
  47. planar/ai/agent_utils.py +205 -0
  48. planar/ai/models.py +140 -0
  49. planar/ai/providers.py +1088 -0
  50. planar/ai/test_agent.py +1298 -0
  51. planar/ai/test_agent_serialization.py +229 -0
  52. planar/ai/test_providers.py +463 -0
  53. planar/ai/utils.py +102 -0
  54. planar/app.py +494 -0
  55. planar/cli.py +282 -0
  56. planar/config.py +544 -0
  57. planar/db/.db.py.un~ +0 -0
  58. planar/db/__init__.py +17 -0
  59. planar/db/alembic/env.py +136 -0
  60. planar/db/alembic/script.py.mako +28 -0
  61. planar/db/alembic/versions/3476068c153c_initial_system_tables_migration.py +339 -0
  62. planar/db/alembic.ini +128 -0
  63. planar/db/db.py +318 -0
  64. planar/files/.config.py.un~ +0 -0
  65. planar/files/.local.py.un~ +0 -0
  66. planar/files/.local_filesystem.py.un~ +0 -0
  67. planar/files/.model.py.un~ +0 -0
  68. planar/files/.models.py.un~ +0 -0
  69. planar/files/.s3.py.un~ +0 -0
  70. planar/files/.storage.py.un~ +0 -0
  71. planar/files/.test_files.py.un~ +0 -0
  72. planar/files/__init__.py +2 -0
  73. planar/files/models.py +162 -0
  74. planar/files/storage/.__init__.py.un~ +0 -0
  75. planar/files/storage/.base.py.un~ +0 -0
  76. planar/files/storage/.config.py.un~ +0 -0
  77. planar/files/storage/.context.py.un~ +0 -0
  78. planar/files/storage/.local_directory.py.un~ +0 -0
  79. planar/files/storage/.test_local_directory.py.un~ +0 -0
  80. planar/files/storage/.test_s3.py.un~ +0 -0
  81. planar/files/storage/base.py +61 -0
  82. planar/files/storage/config.py +44 -0
  83. planar/files/storage/context.py +15 -0
  84. planar/files/storage/local_directory.py +188 -0
  85. planar/files/storage/s3.py +220 -0
  86. planar/files/storage/test_local_directory.py +162 -0
  87. planar/files/storage/test_s3.py +299 -0
  88. planar/files/test_files.py +283 -0
  89. planar/human/.human.py.un~ +0 -0
  90. planar/human/.test_human.py.un~ +0 -0
  91. planar/human/__init__.py +2 -0
  92. planar/human/human.py +458 -0
  93. planar/human/models.py +80 -0
  94. planar/human/test_human.py +385 -0
  95. planar/logging/.__init__.py.un~ +0 -0
  96. planar/logging/.attributes.py.un~ +0 -0
  97. planar/logging/.formatter.py.un~ +0 -0
  98. planar/logging/.logger.py.un~ +0 -0
  99. planar/logging/.otel.py.un~ +0 -0
  100. planar/logging/.tracer.py.un~ +0 -0
  101. planar/logging/__init__.py +10 -0
  102. planar/logging/attributes.py +54 -0
  103. planar/logging/context.py +14 -0
  104. planar/logging/formatter.py +113 -0
  105. planar/logging/logger.py +114 -0
  106. planar/logging/otel.py +51 -0
  107. planar/modeling/.mixin.py.un~ +0 -0
  108. planar/modeling/.storage.py.un~ +0 -0
  109. planar/modeling/__init__.py +0 -0
  110. planar/modeling/field_helpers.py +59 -0
  111. planar/modeling/json_schema_generator.py +94 -0
  112. planar/modeling/mixins/__init__.py +10 -0
  113. planar/modeling/mixins/auditable.py +52 -0
  114. planar/modeling/mixins/test_auditable.py +97 -0
  115. planar/modeling/mixins/test_timestamp.py +134 -0
  116. planar/modeling/mixins/test_uuid_primary_key.py +52 -0
  117. planar/modeling/mixins/timestamp.py +53 -0
  118. planar/modeling/mixins/uuid_primary_key.py +19 -0
  119. planar/modeling/orm/.planar_base_model.py.un~ +0 -0
  120. planar/modeling/orm/__init__.py +18 -0
  121. planar/modeling/orm/planar_base_entity.py +29 -0
  122. planar/modeling/orm/query_filter_builder.py +122 -0
  123. planar/modeling/orm/reexports.py +15 -0
  124. planar/object_config/.object_config.py.un~ +0 -0
  125. planar/object_config/__init__.py +11 -0
  126. planar/object_config/models.py +114 -0
  127. planar/object_config/object_config.py +378 -0
  128. planar/object_registry.py +100 -0
  129. planar/registry_items.py +65 -0
  130. planar/routers/.__init__.py.un~ +0 -0
  131. planar/routers/.agents_router.py.un~ +0 -0
  132. planar/routers/.crud.py.un~ +0 -0
  133. planar/routers/.decision.py.un~ +0 -0
  134. planar/routers/.event.py.un~ +0 -0
  135. planar/routers/.file_attachment.py.un~ +0 -0
  136. planar/routers/.files.py.un~ +0 -0
  137. planar/routers/.files_router.py.un~ +0 -0
  138. planar/routers/.human.py.un~ +0 -0
  139. planar/routers/.info.py.un~ +0 -0
  140. planar/routers/.models.py.un~ +0 -0
  141. planar/routers/.object_config_router.py.un~ +0 -0
  142. planar/routers/.rule.py.un~ +0 -0
  143. planar/routers/.test_object_config_router.py.un~ +0 -0
  144. planar/routers/.test_workflow_router.py.un~ +0 -0
  145. planar/routers/.workflow.py.un~ +0 -0
  146. planar/routers/__init__.py +13 -0
  147. planar/routers/agents_router.py +197 -0
  148. planar/routers/entity_router.py +143 -0
  149. planar/routers/event.py +91 -0
  150. planar/routers/files.py +142 -0
  151. planar/routers/human.py +151 -0
  152. planar/routers/info.py +131 -0
  153. planar/routers/models.py +170 -0
  154. planar/routers/object_config_router.py +133 -0
  155. planar/routers/rule.py +108 -0
  156. planar/routers/test_agents_router.py +174 -0
  157. planar/routers/test_object_config_router.py +367 -0
  158. planar/routers/test_routes_security.py +169 -0
  159. planar/routers/test_rule_router.py +470 -0
  160. planar/routers/test_workflow_router.py +274 -0
  161. planar/routers/workflow.py +468 -0
  162. planar/rules/.decorator.py.un~ +0 -0
  163. planar/rules/.runner.py.un~ +0 -0
  164. planar/rules/.test_rules.py.un~ +0 -0
  165. planar/rules/__init__.py +23 -0
  166. planar/rules/decorator.py +184 -0
  167. planar/rules/models.py +355 -0
  168. planar/rules/rule_configuration.py +191 -0
  169. planar/rules/runner.py +64 -0
  170. planar/rules/test_rules.py +750 -0
  171. planar/scaffold_templates/app/__init__.py.j2 +0 -0
  172. planar/scaffold_templates/app/db/entities.py.j2 +11 -0
  173. planar/scaffold_templates/app/flows/process_invoice.py.j2 +67 -0
  174. planar/scaffold_templates/main.py.j2 +13 -0
  175. planar/scaffold_templates/planar.dev.yaml.j2 +34 -0
  176. planar/scaffold_templates/planar.prod.yaml.j2 +28 -0
  177. planar/scaffold_templates/pyproject.toml.j2 +10 -0
  178. planar/security/.jwt_middleware.py.un~ +0 -0
  179. planar/security/auth_context.py +148 -0
  180. planar/security/authorization.py +388 -0
  181. planar/security/default_policies.cedar +77 -0
  182. planar/security/jwt_middleware.py +116 -0
  183. planar/security/security_context.py +18 -0
  184. planar/security/tests/test_authorization_context.py +78 -0
  185. planar/security/tests/test_cedar_basics.py +41 -0
  186. planar/security/tests/test_cedar_policies.py +158 -0
  187. planar/security/tests/test_jwt_principal_context.py +179 -0
  188. planar/session.py +40 -0
  189. planar/sse/.constants.py.un~ +0 -0
  190. planar/sse/.example.html.un~ +0 -0
  191. planar/sse/.hub.py.un~ +0 -0
  192. planar/sse/.model.py.un~ +0 -0
  193. planar/sse/.proxy.py.un~ +0 -0
  194. planar/sse/constants.py +1 -0
  195. planar/sse/example.html +126 -0
  196. planar/sse/hub.py +216 -0
  197. planar/sse/model.py +8 -0
  198. planar/sse/proxy.py +257 -0
  199. planar/task_local.py +37 -0
  200. planar/test_app.py +51 -0
  201. planar/test_cli.py +372 -0
  202. planar/test_config.py +512 -0
  203. planar/test_object_config.py +527 -0
  204. planar/test_object_registry.py +14 -0
  205. planar/test_sqlalchemy.py +158 -0
  206. planar/test_utils.py +105 -0
  207. planar/testing/.client.py.un~ +0 -0
  208. planar/testing/.memory_storage.py.un~ +0 -0
  209. planar/testing/.planar_test_client.py.un~ +0 -0
  210. planar/testing/.predictable_tracer.py.un~ +0 -0
  211. planar/testing/.synchronizable_tracer.py.un~ +0 -0
  212. planar/testing/.test_memory_storage.py.un~ +0 -0
  213. planar/testing/.workflow_observer.py.un~ +0 -0
  214. planar/testing/__init__.py +0 -0
  215. planar/testing/memory_storage.py +78 -0
  216. planar/testing/planar_test_client.py +54 -0
  217. planar/testing/synchronizable_tracer.py +153 -0
  218. planar/testing/test_memory_storage.py +143 -0
  219. planar/testing/workflow_observer.py +73 -0
  220. planar/utils.py +70 -0
  221. planar/workflows/.__init__.py.un~ +0 -0
  222. planar/workflows/.builtin_steps.py.un~ +0 -0
  223. planar/workflows/.concurrency_tracing.py.un~ +0 -0
  224. planar/workflows/.context.py.un~ +0 -0
  225. planar/workflows/.contrib.py.un~ +0 -0
  226. planar/workflows/.decorators.py.un~ +0 -0
  227. planar/workflows/.durable_test.py.un~ +0 -0
  228. planar/workflows/.errors.py.un~ +0 -0
  229. planar/workflows/.events.py.un~ +0 -0
  230. planar/workflows/.exceptions.py.un~ +0 -0
  231. planar/workflows/.execution.py.un~ +0 -0
  232. planar/workflows/.human.py.un~ +0 -0
  233. planar/workflows/.lock.py.un~ +0 -0
  234. planar/workflows/.misc.py.un~ +0 -0
  235. planar/workflows/.model.py.un~ +0 -0
  236. planar/workflows/.models.py.un~ +0 -0
  237. planar/workflows/.notifications.py.un~ +0 -0
  238. planar/workflows/.orchestrator.py.un~ +0 -0
  239. planar/workflows/.runtime.py.un~ +0 -0
  240. planar/workflows/.serialization.py.un~ +0 -0
  241. planar/workflows/.step.py.un~ +0 -0
  242. planar/workflows/.step_core.py.un~ +0 -0
  243. planar/workflows/.sub_workflow_runner.py.un~ +0 -0
  244. planar/workflows/.sub_workflow_scheduler.py.un~ +0 -0
  245. planar/workflows/.test_concurrency.py.un~ +0 -0
  246. planar/workflows/.test_concurrency_detection.py.un~ +0 -0
  247. planar/workflows/.test_human.py.un~ +0 -0
  248. planar/workflows/.test_lock_timeout.py.un~ +0 -0
  249. planar/workflows/.test_orchestrator.py.un~ +0 -0
  250. planar/workflows/.test_race_conditions.py.un~ +0 -0
  251. planar/workflows/.test_serialization.py.un~ +0 -0
  252. planar/workflows/.test_suspend_deserialization.py.un~ +0 -0
  253. planar/workflows/.test_workflow.py.un~ +0 -0
  254. planar/workflows/.tracing.py.un~ +0 -0
  255. planar/workflows/.types.py.un~ +0 -0
  256. planar/workflows/.util.py.un~ +0 -0
  257. planar/workflows/.utils.py.un~ +0 -0
  258. planar/workflows/.workflow.py.un~ +0 -0
  259. planar/workflows/.workflow_wrapper.py.un~ +0 -0
  260. planar/workflows/.wrappers.py.un~ +0 -0
  261. planar/workflows/__init__.py +42 -0
  262. planar/workflows/context.py +44 -0
  263. planar/workflows/contrib.py +190 -0
  264. planar/workflows/decorators.py +217 -0
  265. planar/workflows/events.py +185 -0
  266. planar/workflows/exceptions.py +34 -0
  267. planar/workflows/execution.py +198 -0
  268. planar/workflows/lock.py +229 -0
  269. planar/workflows/misc.py +5 -0
  270. planar/workflows/models.py +154 -0
  271. planar/workflows/notifications.py +96 -0
  272. planar/workflows/orchestrator.py +383 -0
  273. planar/workflows/query.py +256 -0
  274. planar/workflows/serialization.py +409 -0
  275. planar/workflows/step_core.py +373 -0
  276. planar/workflows/step_metadata.py +357 -0
  277. planar/workflows/step_testing_utils.py +86 -0
  278. planar/workflows/sub_workflow_runner.py +191 -0
  279. planar/workflows/test_concurrency_detection.py +120 -0
  280. planar/workflows/test_lock_timeout.py +140 -0
  281. planar/workflows/test_serialization.py +1195 -0
  282. planar/workflows/test_suspend_deserialization.py +231 -0
  283. planar/workflows/test_workflow.py +1967 -0
  284. planar/workflows/tracing.py +106 -0
  285. planar/workflows/wrappers.py +41 -0
  286. planar-0.5.0.dist-info/METADATA +285 -0
  287. planar-0.5.0.dist-info/RECORD +289 -0
  288. planar-0.5.0.dist-info/WHEEL +4 -0
  289. planar-0.5.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,174 @@
1
+ """
2
+ Tests for agent router endpoints.
3
+
4
+ This module tests the agent router endpoints to ensure they work correctly
5
+ with the new serialization changes.
6
+ """
7
+
8
+ import pytest
9
+ from sqlmodel.ext.asyncio.session import AsyncSession
10
+
11
+ from planar.ai.agent import Agent
12
+ from planar.app import PlanarApp
13
+ from planar.config import sqlite_config
14
+ from planar.testing.planar_test_client import PlanarTestClient
15
+
16
+
17
+ @pytest.fixture(name="app")
18
+ def app_fixture():
19
+ """Create a test app with agents."""
20
+ app = PlanarApp(
21
+ config=sqlite_config(":memory:"),
22
+ title="Test app for agent router",
23
+ description="Testing agent endpoints",
24
+ )
25
+
26
+ # Register a simple agent
27
+ simple_agent = Agent(
28
+ name="simple_test_agent",
29
+ system_prompt="Simple system prompt",
30
+ user_prompt="Simple user prompt: {input}",
31
+ model="openai:gpt-4o",
32
+ max_turns=2,
33
+ )
34
+ app.register_agent(simple_agent)
35
+
36
+ # Register an agent with tools
37
+ async def test_tool(param: str) -> str:
38
+ """A test tool."""
39
+ return f"Processed: {param}"
40
+
41
+ agent_with_tools = Agent(
42
+ name="agent_with_tools",
43
+ system_prompt="System with tools",
44
+ user_prompt="User: {input}",
45
+ model="anthropic:claude-3-sonnet",
46
+ max_turns=5,
47
+ tools=[test_tool],
48
+ )
49
+ app.register_agent(agent_with_tools)
50
+
51
+ return app
52
+
53
+
54
+ async def test_get_agents_endpoint(
55
+ client: PlanarTestClient, app: PlanarApp, session: AsyncSession
56
+ ):
57
+ """Test the GET /agents endpoint returns agents with configs field."""
58
+ response = await client.get("/planar/v1/agents/")
59
+ assert response.status_code == 200
60
+
61
+ agents = response.json()
62
+ assert len(agents) == 2
63
+
64
+ # Check first agent
65
+ simple_agent = next(a for a in agents if a["name"] == "simple_test_agent")
66
+ assert simple_agent["name"] == "simple_test_agent"
67
+ assert "configs" in simple_agent
68
+ assert isinstance(simple_agent["configs"], list)
69
+ assert len(simple_agent["configs"]) == 1 # Default config always present
70
+
71
+ # Verify the default config is present and correct
72
+ default_config = simple_agent["configs"][-1]
73
+ assert default_config["version"] == 0
74
+ assert default_config["data"]["system_prompt"] == "Simple system prompt"
75
+ assert default_config["data"]["user_prompt"] == "Simple user prompt: {input}"
76
+ assert default_config["data"]["model"] == "openai:gpt-4o"
77
+ assert default_config["data"]["max_turns"] == 2
78
+
79
+ # Verify removed fields are not present
80
+ assert "system_prompt" not in simple_agent
81
+ assert "user_prompt" not in simple_agent
82
+ assert "model" not in simple_agent
83
+ assert "max_turns" not in simple_agent
84
+ assert "overwrites" not in simple_agent
85
+
86
+ # Check agent with tools
87
+ tools_agent = next(a for a in agents if a["name"] == "agent_with_tools")
88
+ assert len(tools_agent["tool_definitions"]) == 1
89
+ assert tools_agent["tool_definitions"][0]["name"] == "test_tool"
90
+
91
+
92
+ async def test_update_agent_endpoint(
93
+ client: PlanarTestClient, app: PlanarApp, session: AsyncSession
94
+ ):
95
+ """Test the PATCH /agents/{agent_name} endpoint creates configs."""
96
+ # Get agents first
97
+ response = await client.get("/planar/v1/agents/")
98
+ assert response.status_code == 200
99
+ agents = response.json()
100
+ assert len(agents) == 2
101
+
102
+ # Update the agent
103
+ update_data = {
104
+ "system_prompt": "Updated system prompt",
105
+ "user_prompt": "Updated user prompt: {input}",
106
+ }
107
+ response = await client.patch(
108
+ "/planar/v1/agents/simple_test_agent", json=update_data
109
+ )
110
+ assert response.status_code == 200
111
+
112
+ updated_agent = response.json()
113
+ assert "configs" in updated_agent
114
+ assert len(updated_agent["configs"]) == 2
115
+
116
+ # Check the config data
117
+ config = updated_agent["configs"][0]
118
+ assert config["data"]["system_prompt"] == "Updated system prompt"
119
+ assert config["data"]["user_prompt"] == "Updated user prompt: {input}"
120
+ assert config["version"] == 1
121
+ assert config["object_type"] == "agent"
122
+ assert config["object_name"] == "simple_test_agent"
123
+
124
+
125
+ async def test_agent_with_multiple_configs(
126
+ client: PlanarTestClient, app: PlanarApp, session: AsyncSession
127
+ ):
128
+ """Test that agents return all configs when multiple exist."""
129
+ # Get the agent ID first
130
+ response = await client.get("/planar/v1/agents/")
131
+ agents = response.json()
132
+ simple_agent = next(a for a in agents if a["name"] == "simple_test_agent")
133
+
134
+ # Create first config via PATCH endpoint
135
+ config1_data = {
136
+ "system_prompt": "Config 1 system",
137
+ "user_prompt": "Config 1 user: {input}",
138
+ "model": "openai:gpt-4o",
139
+ "max_turns": 2,
140
+ "model_parameters": {"temperature": 0.7},
141
+ }
142
+ response = await client.patch(
143
+ f"/planar/v1/agents/{simple_agent['name']}", json=config1_data
144
+ )
145
+ assert response.status_code == 200
146
+
147
+ # Create second config via PATCH endpoint
148
+ config2_data = {
149
+ "system_prompt": "Config 2 system",
150
+ "user_prompt": "Config 2 user: {input}",
151
+ "model": "anthropic:claude-3-opus",
152
+ "max_turns": 4,
153
+ "model_parameters": {"temperature": 0.9},
154
+ }
155
+ response = await client.patch(
156
+ f"/planar/v1/agents/{simple_agent['name']}", json=config2_data
157
+ )
158
+ assert response.status_code == 200
159
+
160
+ # Get agents
161
+ response = await client.get("/planar/v1/agents/")
162
+ agents = response.json()
163
+ simple_agent = next(a for a in agents if a["name"] == "simple_test_agent")
164
+
165
+ # Verify all configs are returned (including default config)
166
+ assert len(simple_agent["configs"]) == 3
167
+ assert simple_agent["configs"][0]["version"] == 2 # Latest first
168
+ assert simple_agent["configs"][1]["version"] == 1
169
+ assert simple_agent["configs"][2]["version"] == 0 # Default config
170
+
171
+ # Verify config data
172
+ assert simple_agent["configs"][0]["data"]["system_prompt"] == "Config 2 system"
173
+ assert simple_agent["configs"][1]["data"]["system_prompt"] == "Config 1 system"
174
+ assert simple_agent["configs"][2]["data"]["system_prompt"] == "Simple system prompt"
@@ -0,0 +1,367 @@
1
+ """
2
+ Tests for object configuration router endpoints.
3
+
4
+ This module tests the object configuration router endpoints to ensure they work correctly
5
+ for both agent and rule configurations.
6
+ """
7
+
8
+ from typing import AsyncGenerator
9
+ from uuid import UUID, uuid4
10
+
11
+ import pytest
12
+ from pydantic import BaseModel, Field
13
+ from sqlmodel.ext.asyncio.session import AsyncSession
14
+
15
+ from planar.ai.agent import Agent
16
+ from planar.ai.agent_utils import agent_configuration
17
+ from planar.ai.models import AgentConfig
18
+ from planar.app import PlanarApp
19
+ from planar.config import sqlite_config
20
+ from planar.object_config import DEFAULT_UUID, ObjectConfiguration
21
+ from planar.object_config.object_config import ConfigurableObjectType
22
+ from planar.object_registry import ObjectRegistry
23
+ from planar.rules.decorator import rule
24
+ from planar.rules.models import Rule, RuleEngineConfig, create_jdm_graph
25
+ from planar.rules.rule_configuration import rule_configuration
26
+ from planar.testing.planar_test_client import PlanarTestClient
27
+
28
+
29
+ class InputForTestRule(BaseModel):
30
+ """Input for test rule."""
31
+
32
+ value: int = Field(description="Test value")
33
+ category: str = Field(description="Test category")
34
+
35
+
36
+ class OutputFromTestRule(BaseModel):
37
+ """Output from test rule."""
38
+
39
+ result: int = Field(description="Result value")
40
+ message: str = Field(description="Result message")
41
+
42
+
43
+ @pytest.fixture(name="app")
44
+ def app_fixture():
45
+ """Create a test app with agents and rules."""
46
+ app = PlanarApp(
47
+ config=sqlite_config(":memory:"),
48
+ title="Test app for object config router",
49
+ description="Testing object configuration endpoints",
50
+ )
51
+
52
+ # Register a simple agent
53
+ simple_agent = Agent(
54
+ name="test_agent",
55
+ system_prompt="Test system prompt",
56
+ user_prompt="Test user prompt: {input}",
57
+ model="openai:gpt-4o",
58
+ max_turns=2,
59
+ )
60
+ app.register_agent(simple_agent)
61
+
62
+ # Create and register a rule
63
+ @rule(description="Test rule for configuration")
64
+ def test_rule(input: InputForTestRule) -> OutputFromTestRule:
65
+ # Default implementation
66
+ return OutputFromTestRule(
67
+ result=input.value * 2, message=f"Processed {input.category}"
68
+ )
69
+
70
+ app.register_rule(test_rule)
71
+
72
+ return app
73
+
74
+
75
+ @pytest.fixture
76
+ async def agent_with_configs(app: PlanarApp, session: AsyncSession):
77
+ """Create an agent with multiple configurations."""
78
+ # First config
79
+ agent_config_1 = AgentConfig(
80
+ system_prompt="Config 1 system",
81
+ user_prompt="Config 1 user: {input}",
82
+ model="openai:gpt-4o",
83
+ max_turns=3,
84
+ )
85
+ await agent_configuration.write_config("test_agent", agent_config_1)
86
+
87
+ # Second config
88
+ agent_config_2 = AgentConfig(
89
+ system_prompt="Config 2 system",
90
+ user_prompt="Config 2 user: {input}",
91
+ model="anthropic:claude-3-sonnet",
92
+ max_turns=5,
93
+ )
94
+ config_2 = await agent_configuration.write_config("test_agent", agent_config_2)
95
+
96
+ # Make the second config active
97
+ await agent_configuration.promote_config(config_2.id)
98
+
99
+ return config_2.id
100
+
101
+
102
+ @pytest.fixture
103
+ async def rule_with_configs(
104
+ session: AsyncSession,
105
+ ) -> AsyncGenerator[tuple[Rule, list[ObjectConfiguration]], None]:
106
+ class RuleInputOutput(BaseModel):
107
+ test: str
108
+
109
+ rule = Rule(
110
+ name=f"test_rule_promote_{uuid4().hex}",
111
+ description="Test rule for promoting configuration",
112
+ input=RuleInputOutput,
113
+ output=RuleInputOutput,
114
+ )
115
+ ObjectRegistry.get_instance().register(rule)
116
+
117
+ # Create some configs
118
+ jdm_config_1 = create_jdm_graph(rule)
119
+ jdm_config_2 = create_jdm_graph(rule)
120
+
121
+ rule_config_1 = RuleEngineConfig(jdm=jdm_config_1)
122
+ rule_config_2 = RuleEngineConfig(jdm=jdm_config_2)
123
+
124
+ config1 = await rule_configuration.write_config(rule.name, rule_config_1)
125
+ config2 = await rule_configuration.write_config(rule.name, rule_config_2)
126
+
127
+ yield rule, [config1, config2]
128
+
129
+
130
+ async def test_promote_agent_config(
131
+ client: PlanarTestClient,
132
+ app: PlanarApp,
133
+ session: AsyncSession,
134
+ agent_with_configs: UUID,
135
+ ):
136
+ """Test promoting an agent configuration."""
137
+ # Get the configurations first to find a non-active one
138
+ agent = app._object_registry.get_agents()[0]
139
+ configs = await agent_configuration.read_configs_with_default(
140
+ "test_agent", agent.to_config()
141
+ )
142
+
143
+ # Find the first (inactive) config
144
+ inactive_config = next(c for c in configs if not c.active)
145
+
146
+ # Promote the inactive config
147
+ request_data = {
148
+ "object_type": ConfigurableObjectType.AGENT,
149
+ "config_id": str(inactive_config.id),
150
+ "object_name": "test_agent",
151
+ }
152
+
153
+ response = await client.post(
154
+ "/planar/v1/object-configurations/promote", json=request_data
155
+ )
156
+ assert response.status_code == 200
157
+
158
+ result = response.json()
159
+ assert "configs" in result
160
+ assert len(result["configs"]) >= 3 # At least 2 configs + default
161
+
162
+ # Verify the promoted config is now active
163
+ promoted_config = next(
164
+ c for c in result["configs"] if c["id"] == str(inactive_config.id)
165
+ )
166
+ assert promoted_config["active"] is True
167
+
168
+ # Verify other configs are inactive
169
+ for config in result["configs"]:
170
+ if config["id"] != str(inactive_config.id):
171
+ assert config["active"] is False
172
+
173
+
174
+ async def test_promote_rule_config(
175
+ client: PlanarTestClient,
176
+ app: PlanarApp,
177
+ session: AsyncSession,
178
+ ):
179
+ """Test promoting a rule configuration."""
180
+ # Get the configurations first to find a non-active one
181
+ rule = ObjectRegistry.get_instance().get_rules()[0]
182
+
183
+ await rule_configuration.write_config(
184
+ rule.name, RuleEngineConfig(jdm=create_jdm_graph(rule))
185
+ )
186
+
187
+ configs = await rule_configuration.read_configs_with_default(
188
+ rule.name, rule.to_config()
189
+ )
190
+
191
+ assert len(configs) == 2
192
+
193
+ # Find the first (inactive) config
194
+ inactive_config = next(c for c in configs if not c.active)
195
+
196
+ # Promote the inactive config
197
+ request_data = {
198
+ "object_type": ConfigurableObjectType.RULE,
199
+ "config_id": str(inactive_config.id),
200
+ "object_name": rule.name,
201
+ }
202
+
203
+ response = await client.post(
204
+ "/planar/v1/object-configurations/promote", json=request_data
205
+ )
206
+ assert response.status_code == 200
207
+
208
+ result = response.json()
209
+ assert "configs" in result
210
+ assert len(result["configs"]) == 2
211
+
212
+ # Verify the promoted config is now active
213
+ promoted_config = next(
214
+ c for c in result["configs"] if c["id"] == str(inactive_config.id)
215
+ )
216
+ assert promoted_config["active"] is True
217
+
218
+ # Verify the config data is correct
219
+ assert promoted_config["object_type"] == "rule"
220
+ assert promoted_config["object_name"] == rule.name
221
+ assert "jdm" in promoted_config["data"]
222
+
223
+
224
+ async def test_promote_to_default_agent(
225
+ client: PlanarTestClient,
226
+ app: PlanarApp,
227
+ session: AsyncSession,
228
+ agent_with_configs: UUID,
229
+ ):
230
+ """Test promoting to default (revert to original implementation) for agent."""
231
+ # Promote to default using the special UUID
232
+ request_data = {
233
+ "object_type": ConfigurableObjectType.AGENT,
234
+ "config_id": str(DEFAULT_UUID),
235
+ "object_name": "test_agent",
236
+ }
237
+
238
+ response = await client.post(
239
+ "/planar/v1/object-configurations/promote", json=request_data
240
+ )
241
+ assert response.status_code == 200
242
+
243
+ result = response.json()
244
+ assert "configs" in result
245
+
246
+ # Verify all non-default configs are inactive
247
+ for config in result["configs"]:
248
+ if config["version"] == 0: # Default config
249
+ assert config["active"] is True
250
+ else:
251
+ assert config["active"] is False
252
+
253
+
254
+ async def test_promote_to_default_rule(
255
+ client: PlanarTestClient,
256
+ app: PlanarApp,
257
+ session: AsyncSession,
258
+ rule_with_configs: UUID,
259
+ ):
260
+ """Test promoting to default (revert to original implementation) for rule."""
261
+ # Promote to default using the special UUID
262
+ request_data = {
263
+ "object_type": ConfigurableObjectType.RULE,
264
+ "config_id": str(DEFAULT_UUID),
265
+ "object_name": "test_rule",
266
+ }
267
+
268
+ response = await client.post(
269
+ "/planar/v1/object-configurations/promote", json=request_data
270
+ )
271
+ assert response.status_code == 200
272
+
273
+ result = response.json()
274
+ assert "configs" in result
275
+
276
+ # Verify all non-default configs are inactive
277
+ for config in result["configs"]:
278
+ if config["version"] == 0: # Default config
279
+ assert config["active"] is True
280
+ else:
281
+ assert config["active"] is False
282
+
283
+
284
+ async def test_promote_nonexistent_agent(
285
+ client: PlanarTestClient, app: PlanarApp, session: AsyncSession
286
+ ):
287
+ """Test promoting config for non-existent agent."""
288
+ request_data = {
289
+ "object_type": ConfigurableObjectType.AGENT,
290
+ "config_id": str(UUID("12345678-1234-5678-1234-567812345678")),
291
+ "object_name": "nonexistent_agent",
292
+ }
293
+
294
+ response = await client.post(
295
+ "/planar/v1/object-configurations/promote", json=request_data
296
+ )
297
+ assert response.status_code == 404
298
+ assert response.json()["detail"] == "Agent not found"
299
+
300
+
301
+ async def test_promote_nonexistent_rule(
302
+ client: PlanarTestClient, app: PlanarApp, session: AsyncSession
303
+ ):
304
+ """Test promoting config for non-existent rule."""
305
+ request_data = {
306
+ "object_type": ConfigurableObjectType.RULE,
307
+ "config_id": str(UUID("12345678-1234-5678-1234-567812345678")),
308
+ "object_name": "nonexistent_rule",
309
+ }
310
+
311
+ response = await client.post(
312
+ "/planar/v1/object-configurations/promote", json=request_data
313
+ )
314
+ assert response.status_code == 404
315
+ assert response.json()["detail"] == "Rule not found"
316
+
317
+
318
+ async def test_promote_nonexistent_config(
319
+ client: PlanarTestClient,
320
+ app: PlanarApp,
321
+ session: AsyncSession,
322
+ agent_with_configs: UUID,
323
+ ):
324
+ """Test promoting a non-existent configuration."""
325
+ # Try to promote a config that doesn't exist
326
+ request_data = {
327
+ "object_type": ConfigurableObjectType.AGENT,
328
+ "config_id": str(UUID("99999999-9999-9999-9999-999999999999")),
329
+ "object_name": "test_agent",
330
+ }
331
+
332
+ # This should fail with an error from the promote_config method
333
+ response = await client.post(
334
+ "/planar/v1/object-configurations/promote", json=request_data
335
+ )
336
+ assert response.status_code == 404
337
+
338
+
339
+ async def test_config_versions_ordering(
340
+ client: PlanarTestClient,
341
+ app: PlanarApp,
342
+ session: AsyncSession,
343
+ agent_with_configs: UUID,
344
+ ):
345
+ """Test that configurations are returned in correct version order."""
346
+ # Promote to ensure we have a known state
347
+ request_data = {
348
+ "object_type": ConfigurableObjectType.AGENT,
349
+ "config_id": str(agent_with_configs),
350
+ "object_name": "test_agent",
351
+ }
352
+
353
+ response = await client.post(
354
+ "/planar/v1/object-configurations/promote", json=request_data
355
+ )
356
+ assert response.status_code == 200
357
+
358
+ result = response.json()
359
+ configs = result["configs"]
360
+
361
+ # Verify configs are ordered by version descending (except default which is always last)
362
+ non_default_configs = [c for c in configs if c["version"] != 0]
363
+ versions = [c["version"] for c in non_default_configs]
364
+ assert versions == sorted(versions, reverse=True)
365
+
366
+ # Verify default config is last
367
+ assert configs[-1]["version"] == 0