planar 0.5.0__py3-none-any.whl → 0.7.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 (206) hide show
  1. planar/_version.py +1 -1
  2. planar/ai/agent.py +67 -30
  3. planar/ai/pydantic_ai.py +570 -0
  4. planar/ai/pydantic_ai_agent.py +329 -0
  5. planar/ai/test_agent.py +2 -2
  6. planar/app.py +64 -20
  7. planar/cli.py +39 -27
  8. planar/config.py +45 -36
  9. planar/db/db.py +2 -1
  10. planar/files/storage/azure_blob.py +343 -0
  11. planar/files/storage/base.py +7 -0
  12. planar/files/storage/config.py +70 -7
  13. planar/files/storage/s3.py +6 -6
  14. planar/files/storage/test_azure_blob.py +435 -0
  15. planar/logging/formatter.py +17 -4
  16. planar/logging/test_formatter.py +327 -0
  17. planar/registry_items.py +2 -1
  18. planar/routers/agents_router.py +3 -1
  19. planar/routers/files.py +11 -2
  20. planar/routers/models.py +14 -1
  21. planar/routers/test_files_router.py +49 -0
  22. planar/routers/test_routes_security.py +5 -7
  23. planar/routers/test_workflow_router.py +270 -3
  24. planar/routers/workflow.py +95 -36
  25. planar/rules/models.py +36 -39
  26. planar/rules/test_data/account_dormancy_management.json +223 -0
  27. planar/rules/test_data/airline_loyalty_points_calculator.json +262 -0
  28. planar/rules/test_data/applicant_risk_assessment.json +435 -0
  29. planar/rules/test_data/booking_fraud_detection.json +407 -0
  30. planar/rules/test_data/cellular_data_rollover_system.json +258 -0
  31. planar/rules/test_data/clinical_trial_eligibility_screener.json +437 -0
  32. planar/rules/test_data/customer_lifetime_value.json +143 -0
  33. planar/rules/test_data/import_duties_calculator.json +289 -0
  34. planar/rules/test_data/insurance_prior_authorization.json +443 -0
  35. planar/rules/test_data/online_check_in_eligibility_system.json +254 -0
  36. planar/rules/test_data/order_consolidation_system.json +375 -0
  37. planar/rules/test_data/portfolio_risk_monitor.json +471 -0
  38. planar/rules/test_data/supply_chain_risk.json +253 -0
  39. planar/rules/test_data/warehouse_cross_docking.json +237 -0
  40. planar/rules/test_rules.py +750 -6
  41. planar/scaffold_templates/planar.dev.yaml.j2 +6 -6
  42. planar/scaffold_templates/planar.prod.yaml.j2 +9 -5
  43. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  44. planar/security/auth_context.py +21 -0
  45. planar/security/{jwt_middleware.py → auth_middleware.py} +70 -17
  46. planar/security/authorization.py +9 -15
  47. planar/security/tests/test_auth_middleware.py +162 -0
  48. planar/sse/proxy.py +4 -9
  49. planar/test_app.py +92 -1
  50. planar/test_cli.py +81 -59
  51. planar/test_config.py +17 -14
  52. planar/testing/fixtures.py +325 -0
  53. planar/testing/planar_test_client.py +5 -2
  54. planar/utils.py +41 -1
  55. planar/workflows/execution.py +1 -1
  56. planar/workflows/orchestrator.py +5 -0
  57. planar/workflows/serialization.py +12 -6
  58. planar/workflows/step_core.py +3 -1
  59. planar/workflows/test_serialization.py +9 -1
  60. {planar-0.5.0.dist-info → planar-0.7.0.dist-info}/METADATA +30 -5
  61. planar-0.7.0.dist-info/RECORD +169 -0
  62. planar/.__init__.py.un~ +0 -0
  63. planar/._version.py.un~ +0 -0
  64. planar/.app.py.un~ +0 -0
  65. planar/.cli.py.un~ +0 -0
  66. planar/.config.py.un~ +0 -0
  67. planar/.context.py.un~ +0 -0
  68. planar/.db.py.un~ +0 -0
  69. planar/.di.py.un~ +0 -0
  70. planar/.engine.py.un~ +0 -0
  71. planar/.files.py.un~ +0 -0
  72. planar/.log_context.py.un~ +0 -0
  73. planar/.log_metadata.py.un~ +0 -0
  74. planar/.logging.py.un~ +0 -0
  75. planar/.object_registry.py.un~ +0 -0
  76. planar/.otel.py.un~ +0 -0
  77. planar/.server.py.un~ +0 -0
  78. planar/.session.py.un~ +0 -0
  79. planar/.sqlalchemy.py.un~ +0 -0
  80. planar/.task_local.py.un~ +0 -0
  81. planar/.test_app.py.un~ +0 -0
  82. planar/.test_config.py.un~ +0 -0
  83. planar/.test_object_config.py.un~ +0 -0
  84. planar/.test_sqlalchemy.py.un~ +0 -0
  85. planar/.test_utils.py.un~ +0 -0
  86. planar/.util.py.un~ +0 -0
  87. planar/.utils.py.un~ +0 -0
  88. planar/ai/.__init__.py.un~ +0 -0
  89. planar/ai/._models.py.un~ +0 -0
  90. planar/ai/.agent.py.un~ +0 -0
  91. planar/ai/.agent_utils.py.un~ +0 -0
  92. planar/ai/.events.py.un~ +0 -0
  93. planar/ai/.files.py.un~ +0 -0
  94. planar/ai/.models.py.un~ +0 -0
  95. planar/ai/.providers.py.un~ +0 -0
  96. planar/ai/.pydantic_ai.py.un~ +0 -0
  97. planar/ai/.pydantic_ai_agent.py.un~ +0 -0
  98. planar/ai/.pydantic_ai_provider.py.un~ +0 -0
  99. planar/ai/.step.py.un~ +0 -0
  100. planar/ai/.test_agent.py.un~ +0 -0
  101. planar/ai/.test_agent_serialization.py.un~ +0 -0
  102. planar/ai/.test_providers.py.un~ +0 -0
  103. planar/ai/.utils.py.un~ +0 -0
  104. planar/db/.db.py.un~ +0 -0
  105. planar/files/.config.py.un~ +0 -0
  106. planar/files/.local.py.un~ +0 -0
  107. planar/files/.local_filesystem.py.un~ +0 -0
  108. planar/files/.model.py.un~ +0 -0
  109. planar/files/.models.py.un~ +0 -0
  110. planar/files/.s3.py.un~ +0 -0
  111. planar/files/.storage.py.un~ +0 -0
  112. planar/files/.test_files.py.un~ +0 -0
  113. planar/files/storage/.__init__.py.un~ +0 -0
  114. planar/files/storage/.base.py.un~ +0 -0
  115. planar/files/storage/.config.py.un~ +0 -0
  116. planar/files/storage/.context.py.un~ +0 -0
  117. planar/files/storage/.local_directory.py.un~ +0 -0
  118. planar/files/storage/.test_local_directory.py.un~ +0 -0
  119. planar/files/storage/.test_s3.py.un~ +0 -0
  120. planar/human/.human.py.un~ +0 -0
  121. planar/human/.test_human.py.un~ +0 -0
  122. planar/logging/.__init__.py.un~ +0 -0
  123. planar/logging/.attributes.py.un~ +0 -0
  124. planar/logging/.formatter.py.un~ +0 -0
  125. planar/logging/.logger.py.un~ +0 -0
  126. planar/logging/.otel.py.un~ +0 -0
  127. planar/logging/.tracer.py.un~ +0 -0
  128. planar/modeling/.mixin.py.un~ +0 -0
  129. planar/modeling/.storage.py.un~ +0 -0
  130. planar/modeling/orm/.planar_base_model.py.un~ +0 -0
  131. planar/object_config/.object_config.py.un~ +0 -0
  132. planar/routers/.__init__.py.un~ +0 -0
  133. planar/routers/.agents_router.py.un~ +0 -0
  134. planar/routers/.crud.py.un~ +0 -0
  135. planar/routers/.decision.py.un~ +0 -0
  136. planar/routers/.event.py.un~ +0 -0
  137. planar/routers/.file_attachment.py.un~ +0 -0
  138. planar/routers/.files.py.un~ +0 -0
  139. planar/routers/.files_router.py.un~ +0 -0
  140. planar/routers/.human.py.un~ +0 -0
  141. planar/routers/.info.py.un~ +0 -0
  142. planar/routers/.models.py.un~ +0 -0
  143. planar/routers/.object_config_router.py.un~ +0 -0
  144. planar/routers/.rule.py.un~ +0 -0
  145. planar/routers/.test_object_config_router.py.un~ +0 -0
  146. planar/routers/.test_workflow_router.py.un~ +0 -0
  147. planar/routers/.workflow.py.un~ +0 -0
  148. planar/rules/.decorator.py.un~ +0 -0
  149. planar/rules/.runner.py.un~ +0 -0
  150. planar/rules/.test_rules.py.un~ +0 -0
  151. planar/security/.jwt_middleware.py.un~ +0 -0
  152. planar/sse/.constants.py.un~ +0 -0
  153. planar/sse/.example.html.un~ +0 -0
  154. planar/sse/.hub.py.un~ +0 -0
  155. planar/sse/.model.py.un~ +0 -0
  156. planar/sse/.proxy.py.un~ +0 -0
  157. planar/testing/.client.py.un~ +0 -0
  158. planar/testing/.memory_storage.py.un~ +0 -0
  159. planar/testing/.planar_test_client.py.un~ +0 -0
  160. planar/testing/.predictable_tracer.py.un~ +0 -0
  161. planar/testing/.synchronizable_tracer.py.un~ +0 -0
  162. planar/testing/.test_memory_storage.py.un~ +0 -0
  163. planar/testing/.workflow_observer.py.un~ +0 -0
  164. planar/workflows/.__init__.py.un~ +0 -0
  165. planar/workflows/.builtin_steps.py.un~ +0 -0
  166. planar/workflows/.concurrency_tracing.py.un~ +0 -0
  167. planar/workflows/.context.py.un~ +0 -0
  168. planar/workflows/.contrib.py.un~ +0 -0
  169. planar/workflows/.decorators.py.un~ +0 -0
  170. planar/workflows/.durable_test.py.un~ +0 -0
  171. planar/workflows/.errors.py.un~ +0 -0
  172. planar/workflows/.events.py.un~ +0 -0
  173. planar/workflows/.exceptions.py.un~ +0 -0
  174. planar/workflows/.execution.py.un~ +0 -0
  175. planar/workflows/.human.py.un~ +0 -0
  176. planar/workflows/.lock.py.un~ +0 -0
  177. planar/workflows/.misc.py.un~ +0 -0
  178. planar/workflows/.model.py.un~ +0 -0
  179. planar/workflows/.models.py.un~ +0 -0
  180. planar/workflows/.notifications.py.un~ +0 -0
  181. planar/workflows/.orchestrator.py.un~ +0 -0
  182. planar/workflows/.runtime.py.un~ +0 -0
  183. planar/workflows/.serialization.py.un~ +0 -0
  184. planar/workflows/.step.py.un~ +0 -0
  185. planar/workflows/.step_core.py.un~ +0 -0
  186. planar/workflows/.sub_workflow_runner.py.un~ +0 -0
  187. planar/workflows/.sub_workflow_scheduler.py.un~ +0 -0
  188. planar/workflows/.test_concurrency.py.un~ +0 -0
  189. planar/workflows/.test_concurrency_detection.py.un~ +0 -0
  190. planar/workflows/.test_human.py.un~ +0 -0
  191. planar/workflows/.test_lock_timeout.py.un~ +0 -0
  192. planar/workflows/.test_orchestrator.py.un~ +0 -0
  193. planar/workflows/.test_race_conditions.py.un~ +0 -0
  194. planar/workflows/.test_serialization.py.un~ +0 -0
  195. planar/workflows/.test_suspend_deserialization.py.un~ +0 -0
  196. planar/workflows/.test_workflow.py.un~ +0 -0
  197. planar/workflows/.tracing.py.un~ +0 -0
  198. planar/workflows/.types.py.un~ +0 -0
  199. planar/workflows/.util.py.un~ +0 -0
  200. planar/workflows/.utils.py.un~ +0 -0
  201. planar/workflows/.workflow.py.un~ +0 -0
  202. planar/workflows/.workflow_wrapper.py.un~ +0 -0
  203. planar/workflows/.wrappers.py.un~ +0 -0
  204. planar-0.5.0.dist-info/RECORD +0 -289
  205. {planar-0.5.0.dist-info → planar-0.7.0.dist-info}/WHEEL +0 -0
  206. {planar-0.5.0.dist-info → planar-0.7.0.dist-info}/entry_points.txt +0 -0
planar/_version.py CHANGED
@@ -1 +1 @@
1
- VERSION = "0.5.0"
1
+ VERSION = "0.7.0"
planar/ai/agent.py CHANGED
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import abc
3
4
  import inspect
4
5
  from dataclasses import dataclass, field
5
6
  from typing import (
6
7
  Any,
7
8
  Callable,
9
+ Coroutine,
8
10
  Dict,
9
11
  List,
10
12
  Type,
@@ -37,7 +39,7 @@ from planar.ai.models import (
37
39
  from planar.ai.providers import Anthropic, Gemini, Model, OpenAI
38
40
  from planar.logging import get_logger
39
41
  from planar.modeling.field_helpers import JsonSchema
40
- from planar.utils import utc_now
42
+ from planar.utils import P, R, T, U, utc_now
41
43
  from planar.workflows import as_step
42
44
  from planar.workflows.models import StepType
43
45
 
@@ -65,11 +67,11 @@ def _parse_model_string(model_str: str) -> Model:
65
67
 
66
68
 
67
69
  @dataclass
68
- class Agent[
70
+ class AgentBase[
69
71
  # TODO: add `= str` default when we upgrade to 3.13
70
72
  TInput: BaseModel | str,
71
73
  TOutput: BaseModel | str,
72
- ]:
74
+ ](abc.ABC):
73
75
  """An LLM-powered agent that can be called directly within workflows."""
74
76
 
75
77
  name: str
@@ -77,10 +79,11 @@ class Agent[
77
79
  output_type: Type[TOutput] | None = None
78
80
  input_type: Type[TInput] | None = None
79
81
  user_prompt: str = ""
80
- model: Union[str, Model] = "openai:gpt-4.1"
81
82
  tools: List[Callable] = field(default_factory=list)
82
83
  max_turns: int = 2
83
84
  model_parameters: Dict[str, Any] = field(default_factory=dict)
85
+ event_emitter: AgentEventEmitter | None = None
86
+ durable: bool = True
84
87
 
85
88
  # TODO: move here to serialize to frontend
86
89
  #
@@ -125,33 +128,37 @@ class Agent[
125
128
  )
126
129
  return self.output_type.model_json_schema()
127
130
 
128
- def to_config(self) -> AgentConfig:
129
- return AgentConfig(
130
- system_prompt=self.system_prompt,
131
- user_prompt=self.user_prompt,
132
- model=str(self.model),
133
- max_turns=self.max_turns,
134
- model_parameters=self.model_parameters,
135
- )
136
-
137
131
  @overload
138
132
  async def __call__(
139
- self: "Agent[TInput, str]",
133
+ self: "AgentBase[TInput, str]",
140
134
  input_value: TInput,
141
- event_emitter: AgentEventEmitter | None = None,
142
135
  ) -> AgentRunResult[str]: ...
143
136
 
144
137
  @overload
145
138
  async def __call__(
146
- self: "Agent[TInput, TOutput]",
139
+ self: "AgentBase[TInput, TOutput]",
147
140
  input_value: TInput,
148
- event_emitter: AgentEventEmitter | None = None,
149
141
  ) -> AgentRunResult[TOutput]: ...
150
142
 
143
+ def as_step_if_durable(
144
+ self,
145
+ func: Callable[P, Coroutine[T, U, R]],
146
+ step_type: StepType,
147
+ display_name: str | None = None,
148
+ return_type: Type[R] | None = None,
149
+ ) -> Callable[P, Coroutine[T, U, R]]:
150
+ if not self.durable:
151
+ return func
152
+ return as_step(
153
+ func,
154
+ step_type=step_type,
155
+ display_name=display_name or self.name,
156
+ return_type=return_type,
157
+ )
158
+
151
159
  async def __call__(
152
160
  self,
153
161
  input_value: TInput,
154
- event_emitter: AgentEventEmitter | None = None,
155
162
  ) -> AgentRunResult[Any]:
156
163
  if self.input_type is not None and not isinstance(input_value, self.input_type):
157
164
  raise ValueError(
@@ -165,31 +172,53 @@ class Agent[
165
172
  )
166
173
 
167
174
  if self.output_type is None:
168
- run_step = as_step(
175
+ run_step = self.as_step_if_durable(
169
176
  self.run_step,
170
177
  step_type=StepType.AGENT,
171
178
  display_name=self.name,
172
179
  return_type=AgentRunResult[str],
173
180
  )
174
181
  else:
175
- run_step = as_step(
182
+ run_step = self.as_step_if_durable(
176
183
  self.run_step,
177
184
  step_type=StepType.AGENT,
178
185
  display_name=self.name,
179
186
  return_type=AgentRunResult[self.output_type],
180
187
  )
181
188
 
182
- result = await run_step(
183
- input_value=input_value,
184
- event_emitter=event_emitter,
185
- )
189
+ result = await run_step(input_value=input_value)
186
190
  # Cast the result to ensure type compatibility
187
191
  return cast(AgentRunResult[TOutput], result)
188
192
 
193
+ @abc.abstractmethod
194
+ async def run_step(
195
+ self,
196
+ input_value: TInput,
197
+ ) -> AgentRunResult[TOutput]: ...
198
+
199
+ @abc.abstractmethod
200
+ def get_model_str(self) -> str: ...
201
+
202
+ def to_config(self) -> AgentConfig:
203
+ return AgentConfig(
204
+ system_prompt=self.system_prompt,
205
+ user_prompt=self.user_prompt,
206
+ model=self.get_model_str(),
207
+ max_turns=self.max_turns,
208
+ model_parameters=self.model_parameters,
209
+ )
210
+
211
+
212
+ @dataclass
213
+ class Agent[
214
+ TInput: BaseModel | str,
215
+ TOutput: BaseModel | str,
216
+ ](AgentBase[TInput, TOutput]):
217
+ model: Union[str, Model] = "openai:gpt-4.1"
218
+
189
219
  async def run_step(
190
220
  self,
191
221
  input_value: TInput,
192
- event_emitter: AgentEventEmitter | None = None,
193
222
  ) -> AgentRunResult[TOutput]:
194
223
  """Execute the agent with the provided inputs.
195
224
 
@@ -200,8 +229,12 @@ class Agent[
200
229
  Returns:
201
230
  AgentRunResult containing the agent's response
202
231
  """
232
+ event_emitter = self.event_emitter
203
233
  logger.debug(
204
- "agent run_step called", agent_name=self.name, input_type=type(input_value)
234
+ "agent run_step called",
235
+ agent_name=self.name,
236
+ input_type=type(input_value),
237
+ config=self.to_config(),
205
238
  )
206
239
  result = None
207
240
 
@@ -309,7 +342,7 @@ class Agent[
309
342
  model=model.model_spec,
310
343
  output_type=output_type_for_provider,
311
344
  )
312
- response = await as_step(
345
+ response = await self.as_step_if_durable(
313
346
  model.provider_class.complete,
314
347
  step_type=StepType.AGENT,
315
348
  return_type=CompletionResponse[output_type_for_provider or str],
@@ -336,7 +369,7 @@ class Agent[
336
369
  logger.debug("agent turn", agent_name=self.name, turns_left=turns_left)
337
370
 
338
371
  # Get model response
339
- response = await as_step(
372
+ response = await self.as_step_if_durable(
340
373
  model.provider_class.complete,
341
374
  step_type=StepType.AGENT,
342
375
  return_type=CompletionResponse[output_type_for_provider or str],
@@ -397,7 +430,7 @@ class Agent[
397
430
  )
398
431
  else:
399
432
  # Execute the tool with the provided arguments
400
- tool_result = await as_step(
433
+ tool_result = await self.as_step_if_durable(
401
434
  tool_fn,
402
435
  step_type=StepType.TOOL_CALL,
403
436
  )(**tool_call.arguments)
@@ -439,7 +472,8 @@ class Agent[
439
472
  expected_type=self.output_type,
440
473
  )
441
474
  raise ValueError(
442
- f"Expected result of type {self.output_type} but got none after tool interactions."
475
+ f"Reached max turns without the expected result of type {self.output_type}. "
476
+ "You may need to increase the max_turns parameter or update the Agent instructions."
443
477
  )
444
478
 
445
479
  if event_emitter:
@@ -455,3 +489,6 @@ class Agent[
455
489
  final_result_type=type(result),
456
490
  )
457
491
  return AgentRunResult[TOutput](output=cast(TOutput, result))
492
+
493
+ def get_model_str(self) -> str:
494
+ return str(self.model)