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
Binary file
Binary file
planar/.app.py.un~ ADDED
Binary file
planar/.cli.py.un~ ADDED
Binary file
planar/.config.py.un~ ADDED
Binary file
planar/.context.py.un~ ADDED
Binary file
planar/.db.py.un~ ADDED
Binary file
planar/.di.py.un~ ADDED
Binary file
planar/.engine.py.un~ ADDED
Binary file
planar/.files.py.un~ ADDED
Binary file
Binary file
Binary file
planar/.logging.py.un~ ADDED
Binary file
Binary file
planar/.otel.py.un~ ADDED
Binary file
planar/.server.py.un~ ADDED
Binary file
planar/.session.py.un~ ADDED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
planar/.util.py.un~ ADDED
Binary file
planar/.utils.py.un~ ADDED
Binary file
planar/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ # Re-export main components
2
+ from .app import PlanarApp
3
+ from .config import (
4
+ AppConfig,
5
+ DatabaseConfig,
6
+ InvalidConfigurationError,
7
+ PlanarConfig,
8
+ PostgreSQLConfig,
9
+ SQLiteConfig,
10
+ load_config,
11
+ sqlite_config,
12
+ )
13
+ from .session import get_session
14
+
15
+ __all__ = [
16
+ "get_session",
17
+ "load_config",
18
+ "sqlite_config",
19
+ "InvalidConfigurationError",
20
+ "PlanarConfig",
21
+ "PlanarApp",
22
+ "AppConfig",
23
+ "DatabaseConfig",
24
+ "SQLiteConfig",
25
+ "PostgreSQLConfig",
26
+ ]
planar/_version.py ADDED
@@ -0,0 +1 @@
1
+ VERSION = "0.5.0"
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
planar/ai/.step.py.un~ ADDED
Binary file
Binary file
Binary file
Binary file
planar/ai/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .agent import (
2
+ Agent,
3
+ AgentRunResult,
4
+ )
5
+ from .agent_utils import (
6
+ agent_configuration,
7
+ get_agent_config,
8
+ )
9
+
10
+ __all__ = [
11
+ "Agent",
12
+ "AgentRunResult",
13
+ "agent_configuration",
14
+ "get_agent_config",
15
+ ]
planar/ai/agent.py ADDED
@@ -0,0 +1,457 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from dataclasses import dataclass, field
5
+ from typing import (
6
+ Any,
7
+ Callable,
8
+ Dict,
9
+ List,
10
+ Type,
11
+ Union,
12
+ cast,
13
+ overload,
14
+ )
15
+
16
+ from pydantic import BaseModel
17
+
18
+ from planar.ai.agent_utils import (
19
+ AgentEventEmitter,
20
+ AgentEventType,
21
+ ToolCallResult,
22
+ create_tool_definition,
23
+ extract_files_from_model,
24
+ get_agent_config,
25
+ render_template,
26
+ )
27
+ from planar.ai.models import (
28
+ AgentConfig,
29
+ AgentRunResult,
30
+ AssistantMessage,
31
+ CompletionResponse,
32
+ ModelMessage,
33
+ SystemMessage,
34
+ ToolResponse,
35
+ UserMessage,
36
+ )
37
+ from planar.ai.providers import Anthropic, Gemini, Model, OpenAI
38
+ from planar.logging import get_logger
39
+ from planar.modeling.field_helpers import JsonSchema
40
+ from planar.utils import utc_now
41
+ from planar.workflows import as_step
42
+ from planar.workflows.models import StepType
43
+
44
+ logger = get_logger(__name__)
45
+
46
+
47
+ def _parse_model_string(model_str: str) -> Model:
48
+ """Parse a model string (e.g., 'openai:gpt-4.1') into a Model instance."""
49
+ parts = model_str.split(":", 1)
50
+ if len(parts) != 2:
51
+ raise ValueError(
52
+ f"Invalid model format: {model_str}. Expected format: 'provider:model_id'"
53
+ )
54
+
55
+ provider_id, model_id = parts
56
+
57
+ if provider_id.lower() == "openai":
58
+ return OpenAI.model(model_id)
59
+ elif provider_id.lower() == "anthropic":
60
+ return Anthropic.model(model_id)
61
+ elif provider_id.lower() == "gemini":
62
+ return Gemini.model(model_id)
63
+ else:
64
+ raise ValueError(f"Unsupported provider: {provider_id}")
65
+
66
+
67
+ @dataclass
68
+ class Agent[
69
+ # TODO: add `= str` default when we upgrade to 3.13
70
+ TInput: BaseModel | str,
71
+ TOutput: BaseModel | str,
72
+ ]:
73
+ """An LLM-powered agent that can be called directly within workflows."""
74
+
75
+ name: str
76
+ system_prompt: str
77
+ output_type: Type[TOutput] | None = None
78
+ input_type: Type[TInput] | None = None
79
+ user_prompt: str = ""
80
+ model: Union[str, Model] = "openai:gpt-4.1"
81
+ tools: List[Callable] = field(default_factory=list)
82
+ max_turns: int = 2
83
+ model_parameters: Dict[str, Any] = field(default_factory=dict)
84
+
85
+ # TODO: move here to serialize to frontend
86
+ #
87
+ # built_in_vars: Dict[str, str] = field(default_factory=lambda: {
88
+ # "datetime_now": datetime.datetime.now().isoformat(),
89
+ # "date_today": datetime.date.today().isoformat(),
90
+ # })
91
+
92
+ def __post_init__(self):
93
+ if self.input_type:
94
+ if (
95
+ not issubclass(self.input_type, BaseModel)
96
+ and self.input_type is not str
97
+ ):
98
+ raise ValueError(
99
+ "input_type must be 'str' or a subclass of a Pydantic model"
100
+ )
101
+ if self.max_turns < 1:
102
+ raise ValueError("Max_turns must be greater than or equal to 1.")
103
+ if self.tools and self.max_turns <= 1:
104
+ raise ValueError(
105
+ "For tool calling to work, max_turns must be greater than 1."
106
+ )
107
+
108
+ def input_schema(self) -> JsonSchema | None:
109
+ if self.input_type is None:
110
+ return None
111
+ if self.input_type is str:
112
+ return None
113
+ assert issubclass(self.input_type, BaseModel), (
114
+ "input_type must be a subclass of BaseModel or str"
115
+ )
116
+ return self.input_type.model_json_schema()
117
+
118
+ def output_schema(self) -> JsonSchema | None:
119
+ if self.output_type is None:
120
+ return None
121
+ if self.output_type is str:
122
+ return None
123
+ assert issubclass(self.output_type, BaseModel), (
124
+ "output_type must be a subclass of BaseModel or str"
125
+ )
126
+ return self.output_type.model_json_schema()
127
+
128
+ def to_config(self) -> AgentConfig:
129
+ return AgentConfig(
130
+ system_prompt=self.system_prompt,
131
+ user_prompt=self.user_prompt,
132
+ model=str(self.model),
133
+ max_turns=self.max_turns,
134
+ model_parameters=self.model_parameters,
135
+ )
136
+
137
+ @overload
138
+ async def __call__(
139
+ self: "Agent[TInput, str]",
140
+ input_value: TInput,
141
+ event_emitter: AgentEventEmitter | None = None,
142
+ ) -> AgentRunResult[str]: ...
143
+
144
+ @overload
145
+ async def __call__(
146
+ self: "Agent[TInput, TOutput]",
147
+ input_value: TInput,
148
+ event_emitter: AgentEventEmitter | None = None,
149
+ ) -> AgentRunResult[TOutput]: ...
150
+
151
+ async def __call__(
152
+ self,
153
+ input_value: TInput,
154
+ event_emitter: AgentEventEmitter | None = None,
155
+ ) -> AgentRunResult[Any]:
156
+ if self.input_type is not None and not isinstance(input_value, self.input_type):
157
+ raise ValueError(
158
+ f"Input value must be of type {self.input_type}, but got {type(input_value)}"
159
+ )
160
+ elif not isinstance(input_value, (str, BaseModel)):
161
+ # Should not happen based on type constraints, but just in case
162
+ # user does not have type checking enabled
163
+ raise ValueError(
164
+ "Input value must be a string or a Pydantic model if input_type is not provided"
165
+ )
166
+
167
+ if self.output_type is None:
168
+ run_step = as_step(
169
+ self.run_step,
170
+ step_type=StepType.AGENT,
171
+ display_name=self.name,
172
+ return_type=AgentRunResult[str],
173
+ )
174
+ else:
175
+ run_step = as_step(
176
+ self.run_step,
177
+ step_type=StepType.AGENT,
178
+ display_name=self.name,
179
+ return_type=AgentRunResult[self.output_type],
180
+ )
181
+
182
+ result = await run_step(
183
+ input_value=input_value,
184
+ event_emitter=event_emitter,
185
+ )
186
+ # Cast the result to ensure type compatibility
187
+ return cast(AgentRunResult[TOutput], result)
188
+
189
+ async def run_step(
190
+ self,
191
+ input_value: TInput,
192
+ event_emitter: AgentEventEmitter | None = None,
193
+ ) -> AgentRunResult[TOutput]:
194
+ """Execute the agent with the provided inputs.
195
+
196
+ Args:
197
+ input_value: The primary input value to the agent, can be a string or Pydantic model
198
+ **kwargs: Alternative way to pass inputs as keyword arguments
199
+
200
+ Returns:
201
+ AgentRunResult containing the agent's response
202
+ """
203
+ logger.debug(
204
+ "agent run_step called", agent_name=self.name, input_type=type(input_value)
205
+ )
206
+ result = None
207
+
208
+ config = await get_agent_config(self.name, self.to_config())
209
+ logger.debug("agent using config", agent_name=self.name, config=config)
210
+
211
+ input_map: dict[str, str | dict[str, Any]] = {}
212
+
213
+ files = extract_files_from_model(input_value)
214
+ logger.debug(
215
+ "extracted files from input for agent",
216
+ num_files=len(files),
217
+ agent_name=self.name,
218
+ )
219
+ match input_value:
220
+ case BaseModel():
221
+ if self.input_type and not isinstance(input_value, self.input_type):
222
+ logger.warning(
223
+ "input value type mismatch for agent",
224
+ agent_name=self.name,
225
+ expected_type=self.input_type,
226
+ got_type=type(input_value),
227
+ )
228
+ raise ValueError(
229
+ f"Input value must be of type {self.input_type}, but got {type(input_value)}"
230
+ )
231
+ input_map["input"] = cast(BaseModel, input_value).model_dump()
232
+ case str():
233
+ input_map["input"] = input_value
234
+ case _:
235
+ logger.warning(
236
+ "unexpected input value type for agent",
237
+ agent_name=self.name,
238
+ type=type(input_value),
239
+ )
240
+ raise ValueError(f"Unexpected input value type: {type(input_value)}")
241
+
242
+ # Add built-in variables
243
+ # TODO: Make deterministic or step
244
+ built_in_vars = {
245
+ "datetime_now": utc_now().isoformat(),
246
+ "date_today": utc_now().date().isoformat(),
247
+ }
248
+ input_map.update(built_in_vars)
249
+
250
+ # Format the prompts with the provided arguments using Jinja templates
251
+ try:
252
+ formatted_system_prompt = (
253
+ render_template(config.system_prompt, input_map)
254
+ if config.system_prompt
255
+ else ""
256
+ )
257
+ formatted_user_prompt = (
258
+ render_template(config.user_prompt, input_map)
259
+ if config.user_prompt
260
+ else ""
261
+ )
262
+ except ValueError as e:
263
+ logger.exception("error formatting prompts for agent", agent_name=self.name)
264
+ raise ValueError(f"Missing required parameter for prompt formatting: {e}")
265
+
266
+ # Get the LLM provider and model
267
+ model_config = config.model
268
+ if isinstance(model_config, str):
269
+ model = _parse_model_string(model_config)
270
+ else:
271
+ model = model_config
272
+
273
+ # Apply model parameters if specified
274
+ if config.model_parameters:
275
+ model = model.with_parameters(**config.model_parameters)
276
+
277
+ # Prepare structured messages
278
+ messages: List[ModelMessage] = []
279
+ if formatted_system_prompt:
280
+ messages.append(SystemMessage(content=formatted_system_prompt))
281
+
282
+ if formatted_user_prompt:
283
+ messages.append(UserMessage(content=formatted_user_prompt, files=files))
284
+
285
+ # Prepare tools if provided
286
+ tool_definitions = None
287
+ if self.tools:
288
+ tool_definitions = [create_tool_definition(tool) for tool in self.tools]
289
+
290
+ # Determine output type for the provider call
291
+ # Pass the Pydantic model type if output_type is a subclass of BaseModel,
292
+ # otherwise pass None (indicating string output is expected).
293
+ output_type_for_provider: Type[BaseModel] | None = None
294
+ # Use issubclass safely by checking if output_type is a type first
295
+ if inspect.isclass(self.output_type) and issubclass(
296
+ self.output_type, BaseModel
297
+ ):
298
+ output_type_for_provider = cast(Type[BaseModel], self.output_type)
299
+
300
+ # Execute the LLM call
301
+ max_turns = config.max_turns
302
+
303
+ # Single turn completion (default case)
304
+ result = None
305
+ if not tool_definitions:
306
+ logger.debug(
307
+ "agent performing single turn completion",
308
+ agent_name=self.name,
309
+ model=model.model_spec,
310
+ output_type=output_type_for_provider,
311
+ )
312
+ response = await as_step(
313
+ model.provider_class.complete,
314
+ step_type=StepType.AGENT,
315
+ return_type=CompletionResponse[output_type_for_provider or str],
316
+ )(
317
+ model_spec=model.model_spec,
318
+ messages=messages,
319
+ output_type=output_type_for_provider,
320
+ )
321
+ result = response.content
322
+
323
+ # Emit response event if event_emitter is provided
324
+ if event_emitter:
325
+ event_emitter.emit(AgentEventType.RESPONSE, response.content)
326
+ else:
327
+ logger.debug(
328
+ "agent performing multi-turn completion with tools",
329
+ agent_name=self.name,
330
+ max_turns=max_turns,
331
+ )
332
+ # Multi-turn with tools
333
+ turns_left = max_turns
334
+ while turns_left > 0:
335
+ turns_left -= 1
336
+ logger.debug("agent turn", agent_name=self.name, turns_left=turns_left)
337
+
338
+ # Get model response
339
+ response = await as_step(
340
+ model.provider_class.complete,
341
+ step_type=StepType.AGENT,
342
+ return_type=CompletionResponse[output_type_for_provider or str],
343
+ )(
344
+ model_spec=model.model_spec,
345
+ messages=messages,
346
+ output_type=output_type_for_provider,
347
+ tools=tool_definitions,
348
+ )
349
+
350
+ # Emit response event if event_emitter is provided
351
+ if event_emitter:
352
+ event_emitter.emit(AgentEventType.RESPONSE, response.content)
353
+
354
+ # If no tool calls or last turn, return content
355
+ if not response.tool_calls or turns_left == 0:
356
+ logger.debug(
357
+ "agent completion: no tool calls or last turn",
358
+ agent_name=self.name,
359
+ has_content=response.content is not None,
360
+ )
361
+ result = response.content
362
+ break
363
+
364
+ # Process tool calls
365
+ logger.debug(
366
+ "agent received tool calls",
367
+ agent_name=self.name,
368
+ num_tool_calls=len(response.tool_calls),
369
+ )
370
+ assistant_message = AssistantMessage(
371
+ content=None,
372
+ tool_calls=response.tool_calls,
373
+ )
374
+ messages.append(assistant_message)
375
+
376
+ # Execute each tool and add tool responses to messages
377
+ for tool_call_idx, tool_call in enumerate(response.tool_calls):
378
+ logger.debug(
379
+ "agent processing tool call",
380
+ agent_name=self.name,
381
+ tool_call_index=tool_call_idx + 1,
382
+ tool_call_id=tool_call.id,
383
+ tool_call_name=tool_call.name,
384
+ )
385
+ # Find the matching tool function
386
+ tool_fn = next(
387
+ (t for t in self.tools if t.__name__ == tool_call.name),
388
+ None,
389
+ )
390
+
391
+ if not tool_fn:
392
+ tool_result = f"Error: Tool '{tool_call.name}' not found."
393
+ logger.warning(
394
+ "tool not found for agent",
395
+ tool_name=tool_call.name,
396
+ agent_name=self.name,
397
+ )
398
+ else:
399
+ # Execute the tool with the provided arguments
400
+ tool_result = await as_step(
401
+ tool_fn,
402
+ step_type=StepType.TOOL_CALL,
403
+ )(**tool_call.arguments)
404
+ logger.info(
405
+ "tool executed by agent",
406
+ tool_name=tool_call.name,
407
+ agent_name=self.name,
408
+ result_type=type(tool_result),
409
+ )
410
+
411
+ # Create a tool response
412
+ tool_response = ToolResponse(
413
+ tool_call_id=tool_call.id or "call_1", content=str(tool_result)
414
+ )
415
+
416
+ # Emit tool response event if event_emitter is provided
417
+ if event_emitter:
418
+ event_emitter.emit(
419
+ AgentEventType.TOOL_RESPONSE,
420
+ ToolCallResult(
421
+ tool_call_id=tool_call.id or "call_1",
422
+ tool_call_name=tool_call.name,
423
+ content=tool_result,
424
+ ),
425
+ )
426
+
427
+ # Convert the tool response to a message based on provider
428
+ tool_message = model.provider_class.format_tool_response(
429
+ tool_response
430
+ )
431
+ messages.append(tool_message)
432
+
433
+ # Continue to next turn
434
+
435
+ if result is None:
436
+ logger.warning(
437
+ "agent completed tool interactions but result is none",
438
+ agent_name=self.name,
439
+ expected_type=self.output_type,
440
+ )
441
+ raise ValueError(
442
+ f"Expected result of type {self.output_type} but got none after tool interactions."
443
+ )
444
+
445
+ if event_emitter:
446
+ event_emitter.emit(AgentEventType.COMPLETED, result)
447
+
448
+ if result is None:
449
+ logger.warning("agent final result is none", agent_name=self.name)
450
+ raise ValueError("No result obtained after tool interactions")
451
+
452
+ logger.info(
453
+ "agent completed",
454
+ agent_name=self.name,
455
+ final_result_type=type(result),
456
+ )
457
+ return AgentRunResult[TOutput](output=cast(TOutput, result))