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
@@ -0,0 +1,378 @@
|
|
1
|
+
"""
|
2
|
+
This module contains the ObjectConfiguration class, which is used to store and manage
|
3
|
+
the configuration of objects in the database.
|
4
|
+
|
5
|
+
"Object" is used to refer to agents, entities, rules, etc
|
6
|
+
|
7
|
+
See ConfigurableObjectType for the different types of objects that can be configured.
|
8
|
+
|
9
|
+
When a config is written for a particular object (uniquely identified by object_name and object_type),
|
10
|
+
this config will be used during workflow execution to drive the behaviour of that boject.
|
11
|
+
|
12
|
+
If no persisted config exists, then the workflow will fallback to the in-memory implementation
|
13
|
+
of that object as specified by the user in the planar sdk.
|
14
|
+
"""
|
15
|
+
|
16
|
+
from __future__ import annotations
|
17
|
+
|
18
|
+
from datetime import datetime, timezone
|
19
|
+
from typing import Any, Callable, Generic, Sequence, Type, TypeVar, cast
|
20
|
+
from uuid import UUID
|
21
|
+
|
22
|
+
from pydantic import BaseModel
|
23
|
+
from sqlmodel import col, select, update
|
24
|
+
|
25
|
+
from planar.logging import get_logger
|
26
|
+
from planar.object_config.models import (
|
27
|
+
ConfigDiagnostics,
|
28
|
+
ConfigurableObjectType,
|
29
|
+
ObjectConfiguration,
|
30
|
+
ObjectConfigurationBase,
|
31
|
+
)
|
32
|
+
from planar.session import get_session
|
33
|
+
|
34
|
+
# Special case: UUID with all zeros means revert to in-memory default (set all configs inactive)
|
35
|
+
DEFAULT_UUID = UUID("00000000-0000-0000-0000-000000000000")
|
36
|
+
|
37
|
+
logger = get_logger(__name__)
|
38
|
+
|
39
|
+
|
40
|
+
T = TypeVar("T", bound="BaseModel")
|
41
|
+
V = TypeVar("V")
|
42
|
+
|
43
|
+
|
44
|
+
def default_validate_config(name: str, config: BaseModel) -> ConfigDiagnostics:
|
45
|
+
return ConfigDiagnostics(is_valid=True, issues=[])
|
46
|
+
|
47
|
+
|
48
|
+
class ObjectConfigurationIO(Generic[T, V]):
|
49
|
+
"""Abstract base class for reading and writing different object configurations.
|
50
|
+
|
51
|
+
This class provides a framework for creating concrete implementations
|
52
|
+
that know how to deserialize configuration data using specific Pydantic models.
|
53
|
+
"""
|
54
|
+
|
55
|
+
def __init__(
|
56
|
+
self,
|
57
|
+
model_class: Type[T],
|
58
|
+
object_type: ConfigurableObjectType,
|
59
|
+
validate_config: Callable[
|
60
|
+
[str, T], ConfigDiagnostics
|
61
|
+
] = default_validate_config,
|
62
|
+
):
|
63
|
+
"""Initialize the reader with a Pydantic model class.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
model_class: The Pydantic model class used for deserialization
|
67
|
+
object_type: The type of object this configuration is for
|
68
|
+
"""
|
69
|
+
self.model_class = model_class
|
70
|
+
self.object_type = object_type
|
71
|
+
self.validate_config = validate_config
|
72
|
+
|
73
|
+
async def _read_configs(
|
74
|
+
self, object_name: str, limit: int | None = 20
|
75
|
+
) -> Sequence[ObjectConfiguration[T]]:
|
76
|
+
"""Read all configurations from the database for a object with schema validation warnings.
|
77
|
+
|
78
|
+
Intended for internal use only since the public API requires a default (in-memory) configuration
|
79
|
+
|
80
|
+
Args:
|
81
|
+
object_name: The name of the object to read configurations for
|
82
|
+
limit: The maximum number of configurations to read
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
A ConfigurationResult containing all configurations ordered by version (descending) and any schema warnings
|
86
|
+
"""
|
87
|
+
session = get_session()
|
88
|
+
|
89
|
+
async with session.begin_read():
|
90
|
+
# Query for all versions of the object configuration, ordered by version descending
|
91
|
+
statement = (
|
92
|
+
select(ObjectConfiguration)
|
93
|
+
.where(ObjectConfiguration.object_name == object_name)
|
94
|
+
.where(ObjectConfiguration.object_type == self.object_type)
|
95
|
+
.order_by(col(ObjectConfiguration.version).desc())
|
96
|
+
.limit(limit)
|
97
|
+
)
|
98
|
+
|
99
|
+
cfgs = (await session.exec(statement)).all()
|
100
|
+
|
101
|
+
for cfg in cfgs:
|
102
|
+
cfg.data = self.model_class.model_validate(cfg.data)
|
103
|
+
|
104
|
+
logger.debug(
|
105
|
+
f"Read {len(cfgs)} configurations for object '{object_name}' of type '{self.object_type}'."
|
106
|
+
)
|
107
|
+
|
108
|
+
return cfgs
|
109
|
+
|
110
|
+
async def read_configs_with_default(
|
111
|
+
self, object_name: str, default_value: T
|
112
|
+
) -> list[ObjectConfigurationBase[T]]:
|
113
|
+
"""Read all configurations for a object with a default configuration.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
object_name: The name of the object for which to read configurations
|
117
|
+
default_value: The default configuration to use if no configurations are found
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
A ConfigurationResult containing all configurations ordered by version (descending)
|
121
|
+
"""
|
122
|
+
logger.debug(
|
123
|
+
"reading configs with default for object",
|
124
|
+
object_name=object_name,
|
125
|
+
object_type=self.object_type,
|
126
|
+
)
|
127
|
+
|
128
|
+
config_list = await self._read_configs(object_name)
|
129
|
+
|
130
|
+
default_config_active = all(not config.active for config in config_list)
|
131
|
+
logger.debug(
|
132
|
+
"default config active status",
|
133
|
+
object_name=object_name,
|
134
|
+
is_active=default_config_active,
|
135
|
+
)
|
136
|
+
|
137
|
+
default_config = ObjectConfigurationBase[T].model_validate(
|
138
|
+
{
|
139
|
+
"id": DEFAULT_UUID,
|
140
|
+
"object_type": self.object_type,
|
141
|
+
"object_name": object_name,
|
142
|
+
"version": 0,
|
143
|
+
"active": default_config_active,
|
144
|
+
"created_at": datetime.now(timezone.utc),
|
145
|
+
"data": default_value,
|
146
|
+
}
|
147
|
+
)
|
148
|
+
|
149
|
+
validated_configs: list[ObjectConfigurationBase[T]] = []
|
150
|
+
|
151
|
+
for config in config_list:
|
152
|
+
validated_configs.append(
|
153
|
+
ObjectConfigurationBase[T].model_validate(
|
154
|
+
{
|
155
|
+
"id": config.id,
|
156
|
+
"object_name": config.object_name,
|
157
|
+
"object_type": config.object_type,
|
158
|
+
"created_at": config.created_at,
|
159
|
+
"version": config.version,
|
160
|
+
"data": self.model_class.model_validate(config.data),
|
161
|
+
"active": config.active,
|
162
|
+
}
|
163
|
+
)
|
164
|
+
)
|
165
|
+
|
166
|
+
validated_configs.append(default_config)
|
167
|
+
|
168
|
+
return validated_configs
|
169
|
+
|
170
|
+
async def write_config(self, object_name: str, config: T) -> ObjectConfiguration:
|
171
|
+
"""Write the configuration to a ObjectConfiguration.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
object_name: The name of the object to write configuration for
|
175
|
+
config: The Pydantic model instance to write
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
A ConfigurationResult containing the written configuration
|
179
|
+
"""
|
180
|
+
logger.debug(
|
181
|
+
"writing config for object",
|
182
|
+
object_name=object_name,
|
183
|
+
object_type=self.object_type,
|
184
|
+
)
|
185
|
+
session = get_session()
|
186
|
+
|
187
|
+
async with session.begin():
|
188
|
+
existing_configs = await self._read_configs(object_name, limit=1)
|
189
|
+
|
190
|
+
if not existing_configs:
|
191
|
+
version = 1
|
192
|
+
else:
|
193
|
+
# Get the highest version number and increment it
|
194
|
+
version = existing_configs[0].version + 1
|
195
|
+
|
196
|
+
logger.debug(
|
197
|
+
"new configuration version",
|
198
|
+
object_name=object_name,
|
199
|
+
version=version,
|
200
|
+
)
|
201
|
+
|
202
|
+
result = self.validate_config(object_name, config)
|
203
|
+
|
204
|
+
if not result.is_valid:
|
205
|
+
raise ConfigValidationError(object_name, self.object_type, result)
|
206
|
+
|
207
|
+
# The JSON codec will handle converting the BaseModel to JSON string
|
208
|
+
object_config = ObjectConfiguration(
|
209
|
+
object_name=object_name,
|
210
|
+
object_type=self.object_type,
|
211
|
+
data=config, # type: ignore[arg-type]
|
212
|
+
version=version,
|
213
|
+
)
|
214
|
+
|
215
|
+
session.add(object_config)
|
216
|
+
logger.info(
|
217
|
+
"configuration written to database",
|
218
|
+
version=version,
|
219
|
+
object_name=object_name,
|
220
|
+
config_id=object_config.id,
|
221
|
+
)
|
222
|
+
return object_config
|
223
|
+
|
224
|
+
async def promote_config(
|
225
|
+
self, config_id: UUID, object_name: str | None = None
|
226
|
+
) -> None:
|
227
|
+
"""Promote a specific configuration to be the active one.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
config_id: The UUID of the configuration to promote.
|
231
|
+
Use UUID('00000000-0000-0000-0000-000000000000') to revert to default implementation.
|
232
|
+
object_name: Required when using the default UUID to specify which object to revert
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
A ConfigurationResult containing all configurations for the object
|
236
|
+
|
237
|
+
Raises:
|
238
|
+
ConfigNotFoundError: If the configuration is not found
|
239
|
+
"""
|
240
|
+
logger.debug(
|
241
|
+
"promoting config",
|
242
|
+
config_id=config_id,
|
243
|
+
object_name=object_name,
|
244
|
+
object_type=self.object_type,
|
245
|
+
)
|
246
|
+
session = get_session()
|
247
|
+
async with session.begin():
|
248
|
+
# Edge case: revert to default in-memory configuration by setting all configs to inactive
|
249
|
+
if config_id == DEFAULT_UUID:
|
250
|
+
if not object_name:
|
251
|
+
raise ValueError(
|
252
|
+
"object_name is required when reverting to default configuration"
|
253
|
+
)
|
254
|
+
logger.info(
|
255
|
+
"reverting object to default configuration",
|
256
|
+
object_name=object_name,
|
257
|
+
object_type=self.object_type,
|
258
|
+
)
|
259
|
+
update_query = (
|
260
|
+
update(ObjectConfiguration)
|
261
|
+
.where(col(ObjectConfiguration.object_name) == object_name)
|
262
|
+
.where(col(ObjectConfiguration.object_type) == self.object_type)
|
263
|
+
.values(active=False)
|
264
|
+
)
|
265
|
+
|
266
|
+
await session.exec(cast(Any, update_query))
|
267
|
+
return
|
268
|
+
|
269
|
+
# First, find the configuration to promote
|
270
|
+
target_config = (
|
271
|
+
await session.exec(
|
272
|
+
select(ObjectConfiguration)
|
273
|
+
.where(ObjectConfiguration.id == config_id)
|
274
|
+
.where(ObjectConfiguration.object_type == self.object_type)
|
275
|
+
)
|
276
|
+
).first()
|
277
|
+
|
278
|
+
if not target_config:
|
279
|
+
logger.warning(
|
280
|
+
"config id not found during promotion",
|
281
|
+
config_id=config_id,
|
282
|
+
object_type=self.object_type,
|
283
|
+
)
|
284
|
+
raise ConfigNotFoundError(config_id, self.object_type)
|
285
|
+
|
286
|
+
logger.info(
|
287
|
+
"found target config to promote",
|
288
|
+
version=target_config.version,
|
289
|
+
object_name=target_config.object_name,
|
290
|
+
)
|
291
|
+
|
292
|
+
config_data = self.model_class.model_validate(target_config.data)
|
293
|
+
result = self.validate_config(target_config.object_name, config_data)
|
294
|
+
|
295
|
+
if not result.is_valid:
|
296
|
+
raise ConfigValidationError(
|
297
|
+
target_config.object_name, self.object_type, result
|
298
|
+
)
|
299
|
+
|
300
|
+
# Set all configurations for this object to inactive
|
301
|
+
all_configs = (
|
302
|
+
await session.exec(
|
303
|
+
select(ObjectConfiguration)
|
304
|
+
.where(ObjectConfiguration.object_name == target_config.object_name)
|
305
|
+
.where(ObjectConfiguration.object_type == self.object_type)
|
306
|
+
)
|
307
|
+
).all()
|
308
|
+
|
309
|
+
for config_item in all_configs:
|
310
|
+
config_item.active = False
|
311
|
+
|
312
|
+
# Set the target configuration to active
|
313
|
+
target_config.active = True
|
314
|
+
logger.info(
|
315
|
+
"config is now active",
|
316
|
+
version=target_config.version,
|
317
|
+
object_name=target_config.object_name,
|
318
|
+
)
|
319
|
+
|
320
|
+
# Add all modified configs to the session
|
321
|
+
for config_item in (
|
322
|
+
all_configs
|
323
|
+
): # Ensure target_config is also added if it was part of all_configs
|
324
|
+
session.add(config_item)
|
325
|
+
|
326
|
+
# Explicitly add target_config if it wasn't part of all_configs (should not happen with current logic)
|
327
|
+
# or if it was modified and needs to be re-added.
|
328
|
+
if (
|
329
|
+
target_config not in all_configs
|
330
|
+
): # Should not be true if logic is correct
|
331
|
+
session.add(target_config)
|
332
|
+
|
333
|
+
return
|
334
|
+
|
335
|
+
|
336
|
+
class ConfigValidationErrorResponse(BaseModel):
|
337
|
+
"""Response model for configuration validation errors."""
|
338
|
+
|
339
|
+
error: str
|
340
|
+
object_name: str
|
341
|
+
object_type: str
|
342
|
+
diagnostics: ConfigDiagnostics
|
343
|
+
|
344
|
+
|
345
|
+
class ConfigValidationError(Exception):
|
346
|
+
"""Raised when object configuration validation fails."""
|
347
|
+
|
348
|
+
def __init__(
|
349
|
+
self,
|
350
|
+
object_name: str,
|
351
|
+
object_type: ConfigurableObjectType,
|
352
|
+
diagnostics: ConfigDiagnostics,
|
353
|
+
):
|
354
|
+
self.object_name = object_name
|
355
|
+
self.object_type = object_type
|
356
|
+
self.diagnostics = diagnostics
|
357
|
+
|
358
|
+
super().__init__(f"Validation failed for {object_type} '{object_name}'")
|
359
|
+
|
360
|
+
def to_api_response(self) -> ConfigValidationErrorResponse:
|
361
|
+
"""Convert ValidationError to a JSON-serializable dictionary for API responses."""
|
362
|
+
return ConfigValidationErrorResponse(
|
363
|
+
error="ValidationError",
|
364
|
+
object_name=self.object_name,
|
365
|
+
object_type=self.object_type.value,
|
366
|
+
diagnostics=self.diagnostics,
|
367
|
+
)
|
368
|
+
|
369
|
+
|
370
|
+
class ConfigNotFoundError(Exception):
|
371
|
+
"""Raised when a configuration with the specified ID is not found."""
|
372
|
+
|
373
|
+
def __init__(self, invalid_id, object_type):
|
374
|
+
self.invalid_id = invalid_id
|
375
|
+
self.object_type = object_type
|
376
|
+
super().__init__(
|
377
|
+
f"Configuration with ID {invalid_id} and object_type {object_type} not found"
|
378
|
+
)
|
@@ -0,0 +1,100 @@
|
|
1
|
+
"""
|
2
|
+
Used to track what objects have been registered with a PlanarAppinstance
|
3
|
+
|
4
|
+
Note that in planar/workflows/execution.py, we also have a registry of workflows
|
5
|
+
called _WORKFLOW_FUNCTION_REGISTRY. However, that registry is internal to the implementation
|
6
|
+
of workflows. Do not use that registry.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
from typing import Type
|
12
|
+
|
13
|
+
from planar.ai.agent import Agent
|
14
|
+
from planar.modeling.orm.planar_base_entity import PlanarBaseEntity
|
15
|
+
from planar.registry_items import RegisteredWorkflow
|
16
|
+
from planar.rules.models import Rule
|
17
|
+
from planar.workflows.decorators import WorkflowWrapper
|
18
|
+
|
19
|
+
|
20
|
+
# singleton
|
21
|
+
class ObjectRegistry:
|
22
|
+
_instance: ObjectRegistry | None = None
|
23
|
+
|
24
|
+
_rules: dict[str, Rule]
|
25
|
+
_workflows: dict[str, RegisteredWorkflow]
|
26
|
+
_entities: dict[str, Type[PlanarBaseEntity]]
|
27
|
+
_agents: dict[str, Agent]
|
28
|
+
|
29
|
+
def __new__(cls) -> ObjectRegistry:
|
30
|
+
if cls._instance is None:
|
31
|
+
cls._instance = super(ObjectRegistry, cls).__new__(cls)
|
32
|
+
|
33
|
+
cls._instance._rules = {}
|
34
|
+
cls._instance._workflows = {}
|
35
|
+
cls._instance._entities = {}
|
36
|
+
cls._instance._agents = {}
|
37
|
+
|
38
|
+
return cls._instance
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def get_instance() -> ObjectRegistry:
|
42
|
+
if ObjectRegistry._instance is None:
|
43
|
+
return ObjectRegistry()
|
44
|
+
|
45
|
+
return ObjectRegistry._instance
|
46
|
+
|
47
|
+
def register(
|
48
|
+
self, obj: Type[PlanarBaseEntity] | "WorkflowWrapper" | "Agent" | "Rule"
|
49
|
+
) -> None:
|
50
|
+
"""
|
51
|
+
Register a PlanarBaseEntity or WorkflowWrapper object.
|
52
|
+
Adding the same object more than once is a no-op.
|
53
|
+
|
54
|
+
Note that when registering a WorkflowWrapper, its rules are also registered as there is no
|
55
|
+
explicit registration of rules API. It's implicit the moment a rule function (@rule) is invoked
|
56
|
+
from within a workflow.
|
57
|
+
"""
|
58
|
+
|
59
|
+
if isinstance(obj, type) and issubclass(obj, PlanarBaseEntity):
|
60
|
+
self._entities[obj.__name__] = obj
|
61
|
+
elif isinstance(obj, WorkflowWrapper):
|
62
|
+
self._workflows[obj.function_name] = RegisteredWorkflow.from_workflow(obj)
|
63
|
+
elif isinstance(obj, Agent):
|
64
|
+
self._agents[obj.name] = obj
|
65
|
+
elif isinstance(obj, Rule):
|
66
|
+
self._rules[obj.name] = obj
|
67
|
+
|
68
|
+
def get_entities(self) -> list[Type[PlanarBaseEntity]]:
|
69
|
+
"""
|
70
|
+
Get all registered PlanarBaseEntity objects.
|
71
|
+
"""
|
72
|
+
return list(self._entities.values())
|
73
|
+
|
74
|
+
def get_workflows(self) -> list[RegisteredWorkflow]:
|
75
|
+
"""
|
76
|
+
Get all registered WorkflowWrapper objects.
|
77
|
+
"""
|
78
|
+
return list(self._workflows.values())
|
79
|
+
|
80
|
+
def get_rules(self) -> list[Rule]:
|
81
|
+
"""
|
82
|
+
Get all registered rule objects.
|
83
|
+
"""
|
84
|
+
return list(self._rules.values())
|
85
|
+
|
86
|
+
def get_agents(self) -> list[Agent]:
|
87
|
+
"""
|
88
|
+
Get all registered Agent objects.
|
89
|
+
"""
|
90
|
+
return list(self._agents.values())
|
91
|
+
|
92
|
+
def reset(self) -> None:
|
93
|
+
"""
|
94
|
+
Reset the registry by clearing all registered objects.
|
95
|
+
This is useful for testing to ensure clean state between tests.
|
96
|
+
"""
|
97
|
+
self._rules = {}
|
98
|
+
self._workflows = {}
|
99
|
+
self._entities = {}
|
100
|
+
self._agents = {}
|
planar/registry_items.py
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
"""
|
2
|
+
Registry item classes for tracking registered objects.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import inspect
|
8
|
+
from dataclasses import dataclass
|
9
|
+
from typing import Any, Type, cast
|
10
|
+
|
11
|
+
from pydantic import BaseModel, create_model
|
12
|
+
|
13
|
+
from planar.modeling.json_schema_generator import (
|
14
|
+
generate_json_schema_for_input_parameters,
|
15
|
+
generate_json_schema_for_output_parameters,
|
16
|
+
)
|
17
|
+
from planar.utils import snake_case_to_camel_case
|
18
|
+
from planar.workflows.decorators import WorkflowWrapper
|
19
|
+
|
20
|
+
|
21
|
+
def create_pydantic_model_for_workflow(workflow: WorkflowWrapper) -> Type[BaseModel]:
|
22
|
+
start_params = inspect.signature(workflow.original_fn).parameters
|
23
|
+
start_fields = cast(
|
24
|
+
Any,
|
25
|
+
{
|
26
|
+
name: (
|
27
|
+
param.annotation,
|
28
|
+
... if param.default == param.empty else param.default,
|
29
|
+
)
|
30
|
+
for name, param in start_params.items()
|
31
|
+
},
|
32
|
+
)
|
33
|
+
|
34
|
+
simple_name = workflow.function_name.split(".")[-1]
|
35
|
+
start_model_name = f"{snake_case_to_camel_case(simple_name)}StartRequest"
|
36
|
+
|
37
|
+
return create_model(start_model_name, **start_fields)
|
38
|
+
|
39
|
+
|
40
|
+
@dataclass(eq=False)
|
41
|
+
class RegisteredWorkflow:
|
42
|
+
"""Lightweight record of a registered workflow."""
|
43
|
+
|
44
|
+
obj: "WorkflowWrapper"
|
45
|
+
name: str
|
46
|
+
description: str
|
47
|
+
input_schema: dict[str, Any]
|
48
|
+
output_schema: dict[str, Any]
|
49
|
+
pydantic_model: Type[BaseModel]
|
50
|
+
|
51
|
+
@staticmethod
|
52
|
+
def from_workflow(workflow: "WorkflowWrapper") -> "RegisteredWorkflow":
|
53
|
+
"""Create a RegisteredWorkflow from a WorkflowWrapper."""
|
54
|
+
return RegisteredWorkflow(
|
55
|
+
obj=workflow,
|
56
|
+
name=workflow.function_name,
|
57
|
+
description=workflow.__doc__ or "No docstring provided for this function.",
|
58
|
+
input_schema=generate_json_schema_for_input_parameters(
|
59
|
+
workflow.original_fn
|
60
|
+
),
|
61
|
+
output_schema=generate_json_schema_for_output_parameters(
|
62
|
+
workflow.original_fn
|
63
|
+
),
|
64
|
+
pydantic_model=create_pydantic_model_for_workflow(workflow),
|
65
|
+
)
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from .event import create_workflow_event_routes
|
2
|
+
from .files import create_files_router
|
3
|
+
from .human import create_human_task_routes
|
4
|
+
from .info import create_info_router
|
5
|
+
from .workflow import create_workflow_router
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"create_workflow_event_routes",
|
9
|
+
"create_human_task_routes",
|
10
|
+
"create_workflow_router",
|
11
|
+
"create_info_router",
|
12
|
+
"create_files_router",
|
13
|
+
]
|