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
planar/ai/utils.py ADDED
@@ -0,0 +1,102 @@
1
+ """
2
+ Utility functions for working with AI models and agents.
3
+
4
+ This module contains helper functions for working with AI models
5
+ and agents, particularly around serialization and representation.
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+ from planar.ai.agent import Agent
11
+ from planar.ai.agent_utils import agent_configuration, create_tool_definition
12
+ from planar.ai.models import AgentSerializeable
13
+ from planar.logging import get_logger
14
+ from planar.object_registry import ObjectRegistry
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ async def serialize_agent(
20
+ agent_obj: Agent,
21
+ ) -> AgentSerializeable:
22
+ """
23
+ Serialize an agent object into AgentSerializeable with schema validation warnings.
24
+
25
+ Creates a serializable representation of an Agent, including all database
26
+ configurations and schema validation status.
27
+
28
+ Args:
29
+ agent_obj: The agent object to serialize
30
+
31
+ Returns:
32
+ AgentSerializeable representation of the agent
33
+ """
34
+ logger.debug("serializing agent", agent_name=agent_obj.name)
35
+ # Process tools if present
36
+ tool_definitions = []
37
+ if agent_obj.tools:
38
+ tool_definitions = [
39
+ create_tool_definition(t).model_dump() for t in agent_obj.tools
40
+ ]
41
+ logger.debug(
42
+ "tool definitions for agent",
43
+ agent_name=agent_obj.name,
44
+ num_tools=len(tool_definitions),
45
+ )
46
+
47
+ input_schema = agent_obj.input_schema()
48
+ result_schema = agent_obj.output_schema()
49
+ logger.debug(
50
+ "agent schema presence",
51
+ input_schema_present=input_schema is not None,
52
+ output_schema_present=result_schema is not None,
53
+ )
54
+
55
+ configs_list = await agent_configuration.read_configs_with_default(
56
+ agent_obj.name, agent_obj.to_config()
57
+ )
58
+ logger.debug(
59
+ "retrieved configurations for agent",
60
+ num_configs=len(configs_list),
61
+ agent_name=agent_obj.name,
62
+ )
63
+
64
+ serializable = AgentSerializeable(
65
+ name=agent_obj.name,
66
+ tool_definitions=tool_definitions,
67
+ input_schema=input_schema,
68
+ output_schema=result_schema,
69
+ configs=configs_list,
70
+ )
71
+ logger.debug("agent serialized successfully", agent_name=agent_obj.name)
72
+ return serializable
73
+
74
+
75
+ async def get_agent_serializable(
76
+ agent_name: str,
77
+ registry: ObjectRegistry,
78
+ ) -> Optional[AgentSerializeable]:
79
+ """
80
+ Look up an agent by name in the registry and serialize it.
81
+
82
+ Args:
83
+ agent_name: The name of the agent to look up
84
+ registry: ObjectRegistry to look up the agent
85
+
86
+ Returns:
87
+ AgentSerializeable representation of the agent, or None if not found
88
+ """
89
+ logger.debug("looking up agent by name", agent_name=agent_name)
90
+ # Find the first agent with matching name, or None if none found
91
+ reg_agent = next(
92
+ (agent for agent in registry.get_agents() if agent.name == agent_name), None
93
+ )
94
+ if not reg_agent:
95
+ logger.debug("agent not found in registry", agent_name=agent_name)
96
+ return None
97
+
98
+ logger.debug(
99
+ "found agent in registry, serializing",
100
+ agent_name=agent_name,
101
+ )
102
+ return await serialize_agent(reg_agent)
planar/app.py ADDED
@@ -0,0 +1,494 @@
1
+ import asyncio
2
+ from asyncio import CancelledError
3
+ from contextlib import asynccontextmanager
4
+ from typing import Any, Callable, Coroutine, Type
5
+
6
+ from fastapi import APIRouter, FastAPI, HTTPException, Request
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import JSONResponse
9
+ from pydantic import BaseModel
10
+ from sqlalchemy.ext.asyncio import AsyncEngine
11
+ from typing_extensions import TypeVar
12
+
13
+ from planar.ai import Agent
14
+ from planar.config import PlanarConfig, load_environment_aware_config
15
+ from planar.db import DatabaseManager
16
+ from planar.files.storage.base import Storage
17
+ from planar.files.storage.config import create_from_config
18
+ from planar.files.storage.context import set_storage
19
+ from planar.logging import get_logger
20
+ from planar.modeling.orm import PlanarBaseEntity
21
+ from planar.object_registry import ObjectRegistry
22
+ from planar.routers import (
23
+ create_files_router,
24
+ create_human_task_routes,
25
+ create_info_router,
26
+ create_workflow_router,
27
+ )
28
+ from planar.routers.agents_router import create_agent_router
29
+ from planar.routers.entity_router import create_entities_router
30
+ from planar.routers.object_config_router import create_object_config_router
31
+ from planar.routers.rule import create_rule_router
32
+ from planar.rules.decorator import RULE_REGISTRY
33
+ from planar.security.authorization import PolicyService, policy_service_context
34
+ from planar.security.jwt_middleware import JWTMiddleware
35
+ from planar.session import config_var, session_context
36
+ from planar.sse.proxy import SSEProxy
37
+ from planar.workflows import (
38
+ Workflow,
39
+ WorkflowNotification,
40
+ WorkflowNotificationCallback,
41
+ WorkflowOrchestrator,
42
+ WorkflowWrapper,
43
+ orchestrator_context,
44
+ workflow_notification_context,
45
+ )
46
+ from planar.workflows.tracing import LoggingTracer, Tracer, tracer_context
47
+
48
+ T = TypeVar("T", bound=BaseModel)
49
+ U = TypeVar("U", bound=BaseModel)
50
+
51
+ logger = get_logger(__name__)
52
+
53
+ PLANAR_BASE_PATH = "/planar"
54
+
55
+
56
+ class PlanarApp:
57
+ def __init__(
58
+ self,
59
+ *,
60
+ config: PlanarConfig | None = None,
61
+ title: str | None = None,
62
+ description: str | None = None,
63
+ on_startup: Callable[[AsyncEngine], Coroutine[Any, Any, None]] | None = None,
64
+ on_workflow_notification: WorkflowNotificationCallback | None = None,
65
+ ):
66
+ # If no config provided, load from environment
67
+ self.config = config or load_environment_aware_config()
68
+ self.config.configure_logging()
69
+
70
+ self.tracer: Tracer | None = None
71
+ self.storage: Storage | None = None
72
+ self.sse_proxy = SSEProxy(self.config)
73
+ self.on_startup = on_startup
74
+ self.on_workflow_notification = on_workflow_notification
75
+ self.fastapi = FastAPI(
76
+ title=title or "Planar API",
77
+ description=description or "Planar API",
78
+ lifespan=self._lifespan,
79
+ )
80
+ self.policy_service: PolicyService | None = None
81
+
82
+ self.db_manager = DatabaseManager(db_url=self.config.connection_url())
83
+
84
+ if self.config.storage:
85
+ self.storage = create_from_config(self.config.storage)
86
+
87
+ # Used to track what objects have been registered with the app instance
88
+ self._object_registry = ObjectRegistry()
89
+
90
+ setup_file_storage_middleware(self)
91
+ setup_sqlalchemy_session_middleware(self)
92
+ setup_orchestrator_middleware(self)
93
+ setup_workflow_notification_middleware(self)
94
+ setup_tracer_middleware(self)
95
+ setup_jwt_middleware(self)
96
+ setup_http_exception_handler(self)
97
+ setup_authorization_policy_service(self)
98
+
99
+ self.router_v1 = APIRouter(
100
+ prefix=f"{PLANAR_BASE_PATH}/v1", tags=["Planar API v1"]
101
+ )
102
+
103
+ self.router_v1.include_router(
104
+ create_entities_router(self._object_registry),
105
+ prefix="/entities",
106
+ )
107
+
108
+ self.router_v1.include_router(
109
+ create_workflow_router(self._object_registry),
110
+ prefix="/workflows",
111
+ )
112
+ self.router_v1.include_router(
113
+ create_rule_router(self._object_registry),
114
+ prefix="/rules",
115
+ )
116
+ self.router_v1.include_router(
117
+ create_agent_router(self._object_registry),
118
+ prefix="/agents",
119
+ )
120
+ self.router_v1.include_router(
121
+ create_object_config_router(self._object_registry),
122
+ prefix="/object-configurations",
123
+ )
124
+ self.router_v1.include_router(
125
+ create_human_task_routes(),
126
+ prefix="/human-tasks",
127
+ )
128
+
129
+ self.router_v1.include_router(
130
+ create_info_router(
131
+ title=title or "Planar API", description=description or "Planar API"
132
+ ),
133
+ prefix="",
134
+ )
135
+
136
+ if self.sse_proxy.hub_url:
137
+ self.router_v1.include_router(
138
+ self.sse_proxy.router,
139
+ prefix="/sse",
140
+ )
141
+
142
+ if self.storage:
143
+ self.router_v1.include_router(
144
+ create_files_router(),
145
+ prefix="/file",
146
+ )
147
+
148
+ self.router_v1.add_api_route(
149
+ "/health", lambda: {"status": "ok"}, methods=["GET"]
150
+ )
151
+
152
+ self.fastapi.include_router(self.router_v1)
153
+
154
+ async def __call__(self, scope, receive, send):
155
+ if scope["type"] == "lifespan":
156
+ # setup cors middleware as late as possible ensuring
157
+ # that it's the first middleware to be called in the middleware stack
158
+ setup_cors_middleware(self)
159
+ try:
160
+ await self.fastapi(scope, receive, send)
161
+ except CancelledError as e:
162
+ logger.info(f"lifespan cancelled: {e}")
163
+ raise e
164
+
165
+ def start_sse(self):
166
+ if not self.sse_proxy.hub_url:
167
+ return
168
+
169
+ def on_workflow_notification(notification: WorkflowNotification):
170
+ workflow_id = (
171
+ notification.data.id
172
+ if isinstance(notification.data, Workflow)
173
+ else notification.data.workflow_id
174
+ )
175
+ self.sse_proxy.push(
176
+ f"{notification.kind.value}:{workflow_id}",
177
+ notification.data.model_dump(mode="json"),
178
+ )
179
+
180
+ if self.on_workflow_notification:
181
+ raise ValueError(
182
+ "on_workflow_notification should not be set when enabling SSE forwarding"
183
+ )
184
+
185
+ self.on_workflow_notification = on_workflow_notification
186
+ self.sse_proxy.start()
187
+
188
+ async def stop_sse(self):
189
+ if self.sse_proxy.hub_url:
190
+ await self.sse_proxy.stop()
191
+
192
+ async def graceful_shutdown(self) -> None:
193
+ """
194
+ Called as soon as the process receives SIGINT/SIGTERM but
195
+ *before* Uvicorn starts waiting for open connections to finish.
196
+
197
+ At the moment we only need to stop the SSE proxy so that
198
+ long-lived EventSource connections close quickly, but more
199
+ early-shutdown logic can be added here in future.
200
+ """
201
+ await self.stop_sse()
202
+
203
+ @asynccontextmanager
204
+ async def _lifespan(self, app: FastAPI):
205
+ self.db_manager.connect()
206
+ await self.db_manager.migrate(
207
+ self.config.use_alembic if self.config.use_alembic is not None else True
208
+ )
209
+
210
+ self.orchestrator = WorkflowOrchestrator(self.db_manager.get_engine())
211
+ config_tok = config_var.set(self.config)
212
+
213
+ self.start_sse()
214
+
215
+ if self.tracer is None:
216
+ self.tracer = LoggingTracer()
217
+
218
+ if self.storage:
219
+ set_storage(self.storage)
220
+
221
+ async with tracer_context(self.tracer):
222
+ self.orchestrator_task = asyncio.create_task(
223
+ self.orchestrator.run(
224
+ notification_callback=self.on_workflow_notification
225
+ )
226
+ )
227
+
228
+ if self.on_startup:
229
+ await self.on_startup(self.db_manager.get_engine())
230
+
231
+ yield
232
+ # stop workflow orchestrator
233
+ self.orchestrator_task.cancel()
234
+ try:
235
+ await self.orchestrator_task
236
+ except asyncio.CancelledError:
237
+ pass
238
+ finally:
239
+ # Reset the config in the context
240
+ config_var.reset(config_tok)
241
+
242
+ await self.db_manager.disconnect()
243
+ logger.info("stopping sse")
244
+ await self.stop_sse()
245
+ logger.info("lifespan completed")
246
+
247
+ def register_agent(self, agent: Agent) -> "PlanarApp":
248
+ self._object_registry.register(agent)
249
+ return self
250
+
251
+ def register_rule(
252
+ self, rule_fn: Callable[[T], Coroutine[Any, Any, U]]
253
+ ) -> "PlanarApp":
254
+ rule = RULE_REGISTRY.get(rule_fn.__name__)
255
+
256
+ if not rule:
257
+ raise ValueError(f"rule {rule_fn.__name__} not found")
258
+
259
+ self._object_registry.register(rule)
260
+
261
+ return self
262
+
263
+ def register_entity(
264
+ self,
265
+ entity_cls: Type[PlanarBaseEntity],
266
+ ) -> "PlanarApp":
267
+ """
268
+ Register an entity. Uses a fluent interface pattern.
269
+
270
+ Args:
271
+ entity_cls: The Planar Entity to create add to the object registry
272
+
273
+ Returns:
274
+ self: Returns the app instance for method chaining
275
+ """
276
+ self._object_registry.register(entity_cls)
277
+
278
+ return self
279
+
280
+ def register_workflow(self, wrapper: WorkflowWrapper) -> "PlanarApp":
281
+ """
282
+ Register routes for starting a workflow and checking its status.
283
+
284
+ Args:
285
+ wrapper: The ``WorkflowWrapper`` containing the workflow definition.
286
+
287
+ Returns:
288
+ self: Returns the service instance for method chaining
289
+ """
290
+ self._object_registry.register(wrapper)
291
+ return self
292
+
293
+ def register_router(
294
+ self,
295
+ router: APIRouter,
296
+ prefix: str | None = None,
297
+ **kwargs,
298
+ ):
299
+ """
300
+ Register a custom router. Uses a fluent interface pattern.
301
+ Args:
302
+ router: APIRouter instance to register
303
+ path_prefix: The URL path prefix for all routes (e.g. '/suppliers')
304
+ Returns:
305
+ self: Returns the app instance for method chaining
306
+ """
307
+ # If router doesn't have tags, create one based on the first word in the route path
308
+ if not getattr(router, "tags", None):
309
+ # Try to derive tags from prefix if available
310
+ if kwargs.get("tags", None) is None and prefix:
311
+ # Extract the first segment of the path (without slashes) as the tag
312
+ tag = prefix.strip("/").split("/")[0].title()
313
+
314
+ if tag:
315
+ kwargs["tags"] = [tag]
316
+ else:
317
+ logger.warning(
318
+ "router being registered without tags. consider adding tags for better api documentation."
319
+ )
320
+
321
+ self.fastapi.include_router(router, prefix=prefix or "", **kwargs)
322
+
323
+ return self
324
+
325
+ @property
326
+ def middleware(self):
327
+ return self.fastapi.middleware
328
+
329
+ async def run_standalone(self, func, *args, **kwargs):
330
+ """
331
+ Run a function in the context of a Planar application.
332
+
333
+ This sets up all the necessary context variables and lifecycle components
334
+ (database, orchestrator, etc.) and then runs the provided async function.
335
+
336
+ Args:
337
+ func: An async function to run
338
+ *args: Arguments to pass to the function
339
+ **kwargs: Keyword arguments to pass to the function
340
+
341
+ Returns:
342
+ The result of the function call
343
+ """
344
+ # Use the same lifespan context manager as the FastAPI app
345
+ async with self._lifespan(self.fastapi):
346
+ # Set up session and orchestrator contexts using the same context managers
347
+ # that are used by the HTTP middlewares
348
+ async with session_context(self.db_manager.get_engine()):
349
+ async with orchestrator_context(self.orchestrator):
350
+ async with policy_service_context(self.policy_service):
351
+ # Run the function with all context properly set up
352
+ return await func(*args, **kwargs)
353
+
354
+
355
+ def setup_file_storage_middleware(app: PlanarApp):
356
+ @app.middleware("http")
357
+ async def file_storage_middleware(request: Request, call_next):
358
+ if app.storage:
359
+ set_storage(app.storage)
360
+ return await call_next(request)
361
+
362
+ return file_storage_middleware
363
+
364
+
365
+ def setup_sqlalchemy_session_middleware(app: PlanarApp):
366
+ @app.middleware("http")
367
+ async def session_middleware(request: Request, call_next):
368
+ async with session_context(app.db_manager.get_engine()):
369
+ response = await call_next(request)
370
+ return response
371
+
372
+ return session_middleware
373
+
374
+
375
+ def setup_orchestrator_middleware(app: PlanarApp):
376
+ @app.middleware("http")
377
+ async def orchestrator_middleware(request: Request, call_next):
378
+ config_tok = config_var.set(app.config)
379
+
380
+ async with orchestrator_context(app.orchestrator):
381
+ response = await call_next(request)
382
+
383
+ config_var.reset(config_tok)
384
+
385
+ return response
386
+
387
+ return orchestrator_middleware
388
+
389
+
390
+ def setup_workflow_notification_middleware(app: PlanarApp):
391
+ # This middleware is used for handling endpoints that start workflows
392
+ @app.middleware("http")
393
+ async def workflow_notification_middleware(request: Request, call_next):
394
+ if not app.on_workflow_notification:
395
+ return await call_next(request)
396
+ async with workflow_notification_context(app.on_workflow_notification):
397
+ return await call_next(request)
398
+
399
+ return workflow_notification_middleware
400
+
401
+
402
+ def setup_tracer_middleware(app: PlanarApp):
403
+ @app.middleware("http")
404
+ async def tracer_middleware(request: Request, call_next):
405
+ if app.tracer:
406
+ async with tracer_context(app.tracer):
407
+ return await call_next(request)
408
+ return await call_next(request)
409
+
410
+ return tracer_middleware
411
+
412
+
413
+ def setup_http_exception_handler(app: PlanarApp):
414
+ """
415
+ This middleware is used to handle HTTP exceptions and return a JSON response
416
+ with the appropriate status code and detail.
417
+
418
+ This is useful for handling HTTP exceptions that are raised by the middleware
419
+ stack. Middleware that uses app.middleware() to register itself already handles
420
+ HTTP exceptions by default. The class based middleware (ie. JWTMiddleware and
421
+ CORSMiddleware) do not handle HTTP exceptions by default.
422
+ """
423
+
424
+ @app.middleware("http")
425
+ async def http_exception_handler(request: Request, call_next):
426
+ try:
427
+ return await call_next(request)
428
+ except HTTPException as e:
429
+ return JSONResponse(
430
+ status_code=e.status_code,
431
+ content={"detail": e.detail} if e.detail else {},
432
+ headers=e.headers,
433
+ )
434
+
435
+
436
+ def setup_cors_middleware(app: PlanarApp):
437
+ opts = {
438
+ "allow_headers": app.config.cors.allow_headers,
439
+ "allow_methods": app.config.cors.allow_methods,
440
+ "allow_credentials": app.config.cors.allow_credentials,
441
+ }
442
+
443
+ if isinstance(app.config.cors.allow_origins, str):
444
+ opts["allow_origin_regex"] = app.config.cors.allow_origins
445
+ else:
446
+ opts["allow_origins"] = app.config.cors.allow_origins
447
+
448
+ app.fastapi.add_middleware(
449
+ CORSMiddleware,
450
+ **opts,
451
+ )
452
+
453
+
454
+ def setup_jwt_middleware(app: PlanarApp):
455
+ if app.config.jwt and app.config.jwt.enabled and app.config.jwt.client_id:
456
+ client_id = app.config.jwt.client_id
457
+ org_id = app.config.jwt.org_id
458
+ additional_exclusion_paths = app.config.jwt.additional_exclusion_paths
459
+ app.fastapi.add_middleware(
460
+ JWTMiddleware, # type: ignore
461
+ client_id,
462
+ org_id,
463
+ additional_exclusion_paths,
464
+ )
465
+ logger.info(
466
+ "jwt middleware enabled",
467
+ client_id=client_id,
468
+ org_id=org_id,
469
+ additional_exclusion_paths=additional_exclusion_paths,
470
+ )
471
+ else:
472
+ logger.warning("JWT middleware disabled")
473
+
474
+
475
+ def setup_authorization_policy_service(app: PlanarApp):
476
+ if app.config.authz and app.config.authz.enabled:
477
+ app.policy_service = PolicyService(
478
+ policy_file_path=app.config.authz.policy_file
479
+ if app.config.authz.policy_file
480
+ else None
481
+ )
482
+ logger.info(
483
+ f"Authorization policy service enabled with policy file: {app.policy_service.policy_file_path}"
484
+ )
485
+ else:
486
+ app.policy_service = None
487
+ logger.warning("Authz service disabled")
488
+
489
+ # Set up middleware to manage authorization service context
490
+ @app.middleware("http")
491
+ async def authz_service_middleware(request: Request, call_next):
492
+ async with policy_service_context(app.policy_service):
493
+ response = await call_next(request)
494
+ return response