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.
- planar/.__init__.py.un~ +0 -0
- planar/._version.py.un~ +0 -0
- planar/.app.py.un~ +0 -0
- planar/.cli.py.un~ +0 -0
- planar/.config.py.un~ +0 -0
- planar/.context.py.un~ +0 -0
- planar/.db.py.un~ +0 -0
- planar/.di.py.un~ +0 -0
- planar/.engine.py.un~ +0 -0
- planar/.files.py.un~ +0 -0
- planar/.log_context.py.un~ +0 -0
- planar/.log_metadata.py.un~ +0 -0
- planar/.logging.py.un~ +0 -0
- planar/.object_registry.py.un~ +0 -0
- planar/.otel.py.un~ +0 -0
- planar/.server.py.un~ +0 -0
- planar/.session.py.un~ +0 -0
- planar/.sqlalchemy.py.un~ +0 -0
- planar/.task_local.py.un~ +0 -0
- planar/.test_app.py.un~ +0 -0
- planar/.test_config.py.un~ +0 -0
- planar/.test_object_config.py.un~ +0 -0
- planar/.test_sqlalchemy.py.un~ +0 -0
- planar/.test_utils.py.un~ +0 -0
- planar/.util.py.un~ +0 -0
- planar/.utils.py.un~ +0 -0
- planar/__init__.py +26 -0
- planar/_version.py +1 -0
- planar/ai/.__init__.py.un~ +0 -0
- planar/ai/._models.py.un~ +0 -0
- planar/ai/.agent.py.un~ +0 -0
- planar/ai/.agent_utils.py.un~ +0 -0
- planar/ai/.events.py.un~ +0 -0
- planar/ai/.files.py.un~ +0 -0
- planar/ai/.models.py.un~ +0 -0
- planar/ai/.providers.py.un~ +0 -0
- planar/ai/.pydantic_ai.py.un~ +0 -0
- planar/ai/.pydantic_ai_agent.py.un~ +0 -0
- planar/ai/.pydantic_ai_provider.py.un~ +0 -0
- planar/ai/.step.py.un~ +0 -0
- planar/ai/.test_agent.py.un~ +0 -0
- planar/ai/.test_agent_serialization.py.un~ +0 -0
- planar/ai/.test_providers.py.un~ +0 -0
- planar/ai/.utils.py.un~ +0 -0
- planar/ai/__init__.py +15 -0
- planar/ai/agent.py +457 -0
- planar/ai/agent_utils.py +205 -0
- planar/ai/models.py +140 -0
- planar/ai/providers.py +1088 -0
- planar/ai/test_agent.py +1298 -0
- planar/ai/test_agent_serialization.py +229 -0
- planar/ai/test_providers.py +463 -0
- planar/ai/utils.py +102 -0
- planar/app.py +494 -0
- planar/cli.py +282 -0
- planar/config.py +544 -0
- planar/db/.db.py.un~ +0 -0
- planar/db/__init__.py +17 -0
- planar/db/alembic/env.py +136 -0
- planar/db/alembic/script.py.mako +28 -0
- planar/db/alembic/versions/3476068c153c_initial_system_tables_migration.py +339 -0
- planar/db/alembic.ini +128 -0
- planar/db/db.py +318 -0
- planar/files/.config.py.un~ +0 -0
- planar/files/.local.py.un~ +0 -0
- planar/files/.local_filesystem.py.un~ +0 -0
- planar/files/.model.py.un~ +0 -0
- planar/files/.models.py.un~ +0 -0
- planar/files/.s3.py.un~ +0 -0
- planar/files/.storage.py.un~ +0 -0
- planar/files/.test_files.py.un~ +0 -0
- planar/files/__init__.py +2 -0
- planar/files/models.py +162 -0
- planar/files/storage/.__init__.py.un~ +0 -0
- planar/files/storage/.base.py.un~ +0 -0
- planar/files/storage/.config.py.un~ +0 -0
- planar/files/storage/.context.py.un~ +0 -0
- planar/files/storage/.local_directory.py.un~ +0 -0
- planar/files/storage/.test_local_directory.py.un~ +0 -0
- planar/files/storage/.test_s3.py.un~ +0 -0
- planar/files/storage/base.py +61 -0
- planar/files/storage/config.py +44 -0
- planar/files/storage/context.py +15 -0
- planar/files/storage/local_directory.py +188 -0
- planar/files/storage/s3.py +220 -0
- planar/files/storage/test_local_directory.py +162 -0
- planar/files/storage/test_s3.py +299 -0
- planar/files/test_files.py +283 -0
- planar/human/.human.py.un~ +0 -0
- planar/human/.test_human.py.un~ +0 -0
- planar/human/__init__.py +2 -0
- planar/human/human.py +458 -0
- planar/human/models.py +80 -0
- planar/human/test_human.py +385 -0
- planar/logging/.__init__.py.un~ +0 -0
- planar/logging/.attributes.py.un~ +0 -0
- planar/logging/.formatter.py.un~ +0 -0
- planar/logging/.logger.py.un~ +0 -0
- planar/logging/.otel.py.un~ +0 -0
- planar/logging/.tracer.py.un~ +0 -0
- planar/logging/__init__.py +10 -0
- planar/logging/attributes.py +54 -0
- planar/logging/context.py +14 -0
- planar/logging/formatter.py +113 -0
- planar/logging/logger.py +114 -0
- planar/logging/otel.py +51 -0
- planar/modeling/.mixin.py.un~ +0 -0
- planar/modeling/.storage.py.un~ +0 -0
- planar/modeling/__init__.py +0 -0
- planar/modeling/field_helpers.py +59 -0
- planar/modeling/json_schema_generator.py +94 -0
- planar/modeling/mixins/__init__.py +10 -0
- planar/modeling/mixins/auditable.py +52 -0
- planar/modeling/mixins/test_auditable.py +97 -0
- planar/modeling/mixins/test_timestamp.py +134 -0
- planar/modeling/mixins/test_uuid_primary_key.py +52 -0
- planar/modeling/mixins/timestamp.py +53 -0
- planar/modeling/mixins/uuid_primary_key.py +19 -0
- planar/modeling/orm/.planar_base_model.py.un~ +0 -0
- planar/modeling/orm/__init__.py +18 -0
- planar/modeling/orm/planar_base_entity.py +29 -0
- planar/modeling/orm/query_filter_builder.py +122 -0
- planar/modeling/orm/reexports.py +15 -0
- planar/object_config/.object_config.py.un~ +0 -0
- planar/object_config/__init__.py +11 -0
- planar/object_config/models.py +114 -0
- planar/object_config/object_config.py +378 -0
- planar/object_registry.py +100 -0
- planar/registry_items.py +65 -0
- planar/routers/.__init__.py.un~ +0 -0
- planar/routers/.agents_router.py.un~ +0 -0
- planar/routers/.crud.py.un~ +0 -0
- planar/routers/.decision.py.un~ +0 -0
- planar/routers/.event.py.un~ +0 -0
- planar/routers/.file_attachment.py.un~ +0 -0
- planar/routers/.files.py.un~ +0 -0
- planar/routers/.files_router.py.un~ +0 -0
- planar/routers/.human.py.un~ +0 -0
- planar/routers/.info.py.un~ +0 -0
- planar/routers/.models.py.un~ +0 -0
- planar/routers/.object_config_router.py.un~ +0 -0
- planar/routers/.rule.py.un~ +0 -0
- planar/routers/.test_object_config_router.py.un~ +0 -0
- planar/routers/.test_workflow_router.py.un~ +0 -0
- planar/routers/.workflow.py.un~ +0 -0
- planar/routers/__init__.py +13 -0
- planar/routers/agents_router.py +197 -0
- planar/routers/entity_router.py +143 -0
- planar/routers/event.py +91 -0
- planar/routers/files.py +142 -0
- planar/routers/human.py +151 -0
- planar/routers/info.py +131 -0
- planar/routers/models.py +170 -0
- planar/routers/object_config_router.py +133 -0
- planar/routers/rule.py +108 -0
- planar/routers/test_agents_router.py +174 -0
- planar/routers/test_object_config_router.py +367 -0
- planar/routers/test_routes_security.py +169 -0
- planar/routers/test_rule_router.py +470 -0
- planar/routers/test_workflow_router.py +274 -0
- planar/routers/workflow.py +468 -0
- planar/rules/.decorator.py.un~ +0 -0
- planar/rules/.runner.py.un~ +0 -0
- planar/rules/.test_rules.py.un~ +0 -0
- planar/rules/__init__.py +23 -0
- planar/rules/decorator.py +184 -0
- planar/rules/models.py +355 -0
- planar/rules/rule_configuration.py +191 -0
- planar/rules/runner.py +64 -0
- planar/rules/test_rules.py +750 -0
- planar/scaffold_templates/app/__init__.py.j2 +0 -0
- planar/scaffold_templates/app/db/entities.py.j2 +11 -0
- planar/scaffold_templates/app/flows/process_invoice.py.j2 +67 -0
- planar/scaffold_templates/main.py.j2 +13 -0
- planar/scaffold_templates/planar.dev.yaml.j2 +34 -0
- planar/scaffold_templates/planar.prod.yaml.j2 +28 -0
- planar/scaffold_templates/pyproject.toml.j2 +10 -0
- planar/security/.jwt_middleware.py.un~ +0 -0
- planar/security/auth_context.py +148 -0
- planar/security/authorization.py +388 -0
- planar/security/default_policies.cedar +77 -0
- planar/security/jwt_middleware.py +116 -0
- planar/security/security_context.py +18 -0
- planar/security/tests/test_authorization_context.py +78 -0
- planar/security/tests/test_cedar_basics.py +41 -0
- planar/security/tests/test_cedar_policies.py +158 -0
- planar/security/tests/test_jwt_principal_context.py +179 -0
- planar/session.py +40 -0
- planar/sse/.constants.py.un~ +0 -0
- planar/sse/.example.html.un~ +0 -0
- planar/sse/.hub.py.un~ +0 -0
- planar/sse/.model.py.un~ +0 -0
- planar/sse/.proxy.py.un~ +0 -0
- planar/sse/constants.py +1 -0
- planar/sse/example.html +126 -0
- planar/sse/hub.py +216 -0
- planar/sse/model.py +8 -0
- planar/sse/proxy.py +257 -0
- planar/task_local.py +37 -0
- planar/test_app.py +51 -0
- planar/test_cli.py +372 -0
- planar/test_config.py +512 -0
- planar/test_object_config.py +527 -0
- planar/test_object_registry.py +14 -0
- planar/test_sqlalchemy.py +158 -0
- planar/test_utils.py +105 -0
- planar/testing/.client.py.un~ +0 -0
- planar/testing/.memory_storage.py.un~ +0 -0
- planar/testing/.planar_test_client.py.un~ +0 -0
- planar/testing/.predictable_tracer.py.un~ +0 -0
- planar/testing/.synchronizable_tracer.py.un~ +0 -0
- planar/testing/.test_memory_storage.py.un~ +0 -0
- planar/testing/.workflow_observer.py.un~ +0 -0
- planar/testing/__init__.py +0 -0
- planar/testing/memory_storage.py +78 -0
- planar/testing/planar_test_client.py +54 -0
- planar/testing/synchronizable_tracer.py +153 -0
- planar/testing/test_memory_storage.py +143 -0
- planar/testing/workflow_observer.py +73 -0
- planar/utils.py +70 -0
- planar/workflows/.__init__.py.un~ +0 -0
- planar/workflows/.builtin_steps.py.un~ +0 -0
- planar/workflows/.concurrency_tracing.py.un~ +0 -0
- planar/workflows/.context.py.un~ +0 -0
- planar/workflows/.contrib.py.un~ +0 -0
- planar/workflows/.decorators.py.un~ +0 -0
- planar/workflows/.durable_test.py.un~ +0 -0
- planar/workflows/.errors.py.un~ +0 -0
- planar/workflows/.events.py.un~ +0 -0
- planar/workflows/.exceptions.py.un~ +0 -0
- planar/workflows/.execution.py.un~ +0 -0
- planar/workflows/.human.py.un~ +0 -0
- planar/workflows/.lock.py.un~ +0 -0
- planar/workflows/.misc.py.un~ +0 -0
- planar/workflows/.model.py.un~ +0 -0
- planar/workflows/.models.py.un~ +0 -0
- planar/workflows/.notifications.py.un~ +0 -0
- planar/workflows/.orchestrator.py.un~ +0 -0
- planar/workflows/.runtime.py.un~ +0 -0
- planar/workflows/.serialization.py.un~ +0 -0
- planar/workflows/.step.py.un~ +0 -0
- planar/workflows/.step_core.py.un~ +0 -0
- planar/workflows/.sub_workflow_runner.py.un~ +0 -0
- planar/workflows/.sub_workflow_scheduler.py.un~ +0 -0
- planar/workflows/.test_concurrency.py.un~ +0 -0
- planar/workflows/.test_concurrency_detection.py.un~ +0 -0
- planar/workflows/.test_human.py.un~ +0 -0
- planar/workflows/.test_lock_timeout.py.un~ +0 -0
- planar/workflows/.test_orchestrator.py.un~ +0 -0
- planar/workflows/.test_race_conditions.py.un~ +0 -0
- planar/workflows/.test_serialization.py.un~ +0 -0
- planar/workflows/.test_suspend_deserialization.py.un~ +0 -0
- planar/workflows/.test_workflow.py.un~ +0 -0
- planar/workflows/.tracing.py.un~ +0 -0
- planar/workflows/.types.py.un~ +0 -0
- planar/workflows/.util.py.un~ +0 -0
- planar/workflows/.utils.py.un~ +0 -0
- planar/workflows/.workflow.py.un~ +0 -0
- planar/workflows/.workflow_wrapper.py.un~ +0 -0
- planar/workflows/.wrappers.py.un~ +0 -0
- planar/workflows/__init__.py +42 -0
- planar/workflows/context.py +44 -0
- planar/workflows/contrib.py +190 -0
- planar/workflows/decorators.py +217 -0
- planar/workflows/events.py +185 -0
- planar/workflows/exceptions.py +34 -0
- planar/workflows/execution.py +198 -0
- planar/workflows/lock.py +229 -0
- planar/workflows/misc.py +5 -0
- planar/workflows/models.py +154 -0
- planar/workflows/notifications.py +96 -0
- planar/workflows/orchestrator.py +383 -0
- planar/workflows/query.py +256 -0
- planar/workflows/serialization.py +409 -0
- planar/workflows/step_core.py +373 -0
- planar/workflows/step_metadata.py +357 -0
- planar/workflows/step_testing_utils.py +86 -0
- planar/workflows/sub_workflow_runner.py +191 -0
- planar/workflows/test_concurrency_detection.py +120 -0
- planar/workflows/test_lock_timeout.py +140 -0
- planar/workflows/test_serialization.py +1195 -0
- planar/workflows/test_suspend_deserialization.py +231 -0
- planar/workflows/test_workflow.py +1967 -0
- planar/workflows/tracing.py +106 -0
- planar/workflows/wrappers.py +41 -0
- planar-0.5.0.dist-info/METADATA +285 -0
- planar-0.5.0.dist-info/RECORD +289 -0
- planar-0.5.0.dist-info/WHEEL +4 -0
- 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
|