mirascope 2.0.0a2__py3-none-any.whl → 2.0.0a4__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 (252) hide show
  1. mirascope/__init__.py +2 -2
  2. mirascope/api/__init__.py +6 -0
  3. mirascope/api/_generated/README.md +207 -0
  4. mirascope/api/_generated/__init__.py +141 -0
  5. mirascope/api/_generated/client.py +163 -0
  6. mirascope/api/_generated/core/__init__.py +52 -0
  7. mirascope/api/_generated/core/api_error.py +23 -0
  8. mirascope/api/_generated/core/client_wrapper.py +58 -0
  9. mirascope/api/_generated/core/datetime_utils.py +30 -0
  10. mirascope/api/_generated/core/file.py +70 -0
  11. mirascope/api/_generated/core/force_multipart.py +16 -0
  12. mirascope/api/_generated/core/http_client.py +619 -0
  13. mirascope/api/_generated/core/http_response.py +55 -0
  14. mirascope/api/_generated/core/jsonable_encoder.py +102 -0
  15. mirascope/api/_generated/core/pydantic_utilities.py +310 -0
  16. mirascope/api/_generated/core/query_encoder.py +60 -0
  17. mirascope/api/_generated/core/remove_none_from_dict.py +11 -0
  18. mirascope/api/_generated/core/request_options.py +35 -0
  19. mirascope/api/_generated/core/serialization.py +282 -0
  20. mirascope/api/_generated/docs/__init__.py +4 -0
  21. mirascope/api/_generated/docs/client.py +95 -0
  22. mirascope/api/_generated/docs/raw_client.py +132 -0
  23. mirascope/api/_generated/environment.py +9 -0
  24. mirascope/api/_generated/errors/__init__.py +17 -0
  25. mirascope/api/_generated/errors/bad_request_error.py +15 -0
  26. mirascope/api/_generated/errors/conflict_error.py +15 -0
  27. mirascope/api/_generated/errors/forbidden_error.py +15 -0
  28. mirascope/api/_generated/errors/internal_server_error.py +15 -0
  29. mirascope/api/_generated/errors/not_found_error.py +15 -0
  30. mirascope/api/_generated/health/__init__.py +7 -0
  31. mirascope/api/_generated/health/client.py +96 -0
  32. mirascope/api/_generated/health/raw_client.py +129 -0
  33. mirascope/api/_generated/health/types/__init__.py +8 -0
  34. mirascope/api/_generated/health/types/health_check_response.py +24 -0
  35. mirascope/api/_generated/health/types/health_check_response_status.py +5 -0
  36. mirascope/api/_generated/organizations/__init__.py +25 -0
  37. mirascope/api/_generated/organizations/client.py +380 -0
  38. mirascope/api/_generated/organizations/raw_client.py +876 -0
  39. mirascope/api/_generated/organizations/types/__init__.py +23 -0
  40. mirascope/api/_generated/organizations/types/organizations_create_response.py +24 -0
  41. mirascope/api/_generated/organizations/types/organizations_create_response_role.py +7 -0
  42. mirascope/api/_generated/organizations/types/organizations_get_response.py +24 -0
  43. mirascope/api/_generated/organizations/types/organizations_get_response_role.py +7 -0
  44. mirascope/api/_generated/organizations/types/organizations_list_response_item.py +24 -0
  45. mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +7 -0
  46. mirascope/api/_generated/organizations/types/organizations_update_response.py +24 -0
  47. mirascope/api/_generated/organizations/types/organizations_update_response_role.py +7 -0
  48. mirascope/api/_generated/projects/__init__.py +17 -0
  49. mirascope/api/_generated/projects/client.py +458 -0
  50. mirascope/api/_generated/projects/raw_client.py +1016 -0
  51. mirascope/api/_generated/projects/types/__init__.py +15 -0
  52. mirascope/api/_generated/projects/types/projects_create_response.py +30 -0
  53. mirascope/api/_generated/projects/types/projects_get_response.py +30 -0
  54. mirascope/api/_generated/projects/types/projects_list_response_item.py +30 -0
  55. mirascope/api/_generated/projects/types/projects_update_response.py +30 -0
  56. mirascope/api/_generated/reference.md +753 -0
  57. mirascope/api/_generated/traces/__init__.py +55 -0
  58. mirascope/api/_generated/traces/client.py +162 -0
  59. mirascope/api/_generated/traces/raw_client.py +168 -0
  60. mirascope/api/_generated/traces/types/__init__.py +95 -0
  61. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item.py +36 -0
  62. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource.py +31 -0
  63. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item.py +25 -0
  64. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value.py +54 -0
  65. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_array_value.py +23 -0
  66. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value.py +28 -0
  67. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value_values_item.py +24 -0
  68. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item.py +35 -0
  69. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope.py +35 -0
  70. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item.py +27 -0
  71. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value.py +54 -0
  72. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_array_value.py +23 -0
  73. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value.py +28 -0
  74. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value_values_item.py +24 -0
  75. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item.py +60 -0
  76. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item.py +29 -0
  77. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value.py +54 -0
  78. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_array_value.py +23 -0
  79. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value.py +28 -0
  80. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value_values_item.py +24 -0
  81. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_status.py +24 -0
  82. mirascope/api/_generated/traces/types/traces_create_response.py +27 -0
  83. mirascope/api/_generated/traces/types/traces_create_response_partial_success.py +28 -0
  84. mirascope/api/_generated/types/__init__.py +37 -0
  85. mirascope/api/_generated/types/already_exists_error.py +24 -0
  86. mirascope/api/_generated/types/already_exists_error_tag.py +5 -0
  87. mirascope/api/_generated/types/database_error.py +24 -0
  88. mirascope/api/_generated/types/database_error_tag.py +5 -0
  89. mirascope/api/_generated/types/http_api_decode_error.py +29 -0
  90. mirascope/api/_generated/types/http_api_decode_error_tag.py +5 -0
  91. mirascope/api/_generated/types/issue.py +40 -0
  92. mirascope/api/_generated/types/issue_tag.py +17 -0
  93. mirascope/api/_generated/types/not_found_error_body.py +24 -0
  94. mirascope/api/_generated/types/not_found_error_tag.py +5 -0
  95. mirascope/api/_generated/types/permission_denied_error.py +24 -0
  96. mirascope/api/_generated/types/permission_denied_error_tag.py +7 -0
  97. mirascope/api/_generated/types/property_key.py +7 -0
  98. mirascope/api/_generated/types/property_key_key.py +27 -0
  99. mirascope/api/_generated/types/property_key_key_tag.py +5 -0
  100. mirascope/api/client.py +255 -0
  101. mirascope/api/settings.py +81 -0
  102. mirascope/llm/__init__.py +45 -11
  103. mirascope/llm/calls/calls.py +81 -57
  104. mirascope/llm/calls/decorator.py +121 -115
  105. mirascope/llm/content/__init__.py +3 -2
  106. mirascope/llm/context/_utils.py +19 -6
  107. mirascope/llm/exceptions.py +30 -16
  108. mirascope/llm/formatting/_utils.py +9 -5
  109. mirascope/llm/formatting/format.py +2 -2
  110. mirascope/llm/formatting/from_call_args.py +2 -2
  111. mirascope/llm/messages/message.py +13 -5
  112. mirascope/llm/models/__init__.py +2 -2
  113. mirascope/llm/models/models.py +189 -81
  114. mirascope/llm/prompts/__init__.py +13 -12
  115. mirascope/llm/prompts/_utils.py +27 -24
  116. mirascope/llm/prompts/decorator.py +133 -204
  117. mirascope/llm/prompts/prompts.py +424 -0
  118. mirascope/llm/prompts/protocols.py +25 -59
  119. mirascope/llm/providers/__init__.py +44 -0
  120. mirascope/llm/{clients → providers}/_missing_import_stubs.py +8 -6
  121. mirascope/llm/providers/anthropic/__init__.py +29 -0
  122. mirascope/llm/providers/anthropic/_utils/__init__.py +23 -0
  123. mirascope/llm/providers/anthropic/_utils/beta_decode.py +271 -0
  124. mirascope/llm/providers/anthropic/_utils/beta_encode.py +216 -0
  125. mirascope/llm/{clients → providers}/anthropic/_utils/decode.py +44 -11
  126. mirascope/llm/providers/anthropic/_utils/encode.py +356 -0
  127. mirascope/llm/providers/anthropic/beta_provider.py +322 -0
  128. mirascope/llm/providers/anthropic/model_id.py +23 -0
  129. mirascope/llm/providers/anthropic/model_info.py +87 -0
  130. mirascope/llm/providers/anthropic/provider.py +416 -0
  131. mirascope/llm/{clients → providers}/base/__init__.py +3 -3
  132. mirascope/llm/{clients → providers}/base/_utils.py +25 -8
  133. mirascope/llm/{clients/base/client.py → providers/base/base_provider.py} +255 -126
  134. mirascope/llm/providers/google/__init__.py +21 -0
  135. mirascope/llm/{clients → providers}/google/_utils/decode.py +61 -7
  136. mirascope/llm/{clients → providers}/google/_utils/encode.py +44 -30
  137. mirascope/llm/providers/google/model_id.py +22 -0
  138. mirascope/llm/providers/google/model_info.py +62 -0
  139. mirascope/llm/providers/google/provider.py +442 -0
  140. mirascope/llm/providers/load_provider.py +54 -0
  141. mirascope/llm/providers/mlx/__init__.py +24 -0
  142. mirascope/llm/providers/mlx/_utils.py +129 -0
  143. mirascope/llm/providers/mlx/encoding/__init__.py +8 -0
  144. mirascope/llm/providers/mlx/encoding/base.py +69 -0
  145. mirascope/llm/providers/mlx/encoding/transformers.py +147 -0
  146. mirascope/llm/providers/mlx/mlx.py +237 -0
  147. mirascope/llm/providers/mlx/model_id.py +17 -0
  148. mirascope/llm/providers/mlx/provider.py +415 -0
  149. mirascope/llm/providers/model_id.py +16 -0
  150. mirascope/llm/providers/ollama/__init__.py +19 -0
  151. mirascope/llm/providers/ollama/provider.py +71 -0
  152. mirascope/llm/providers/openai/__init__.py +6 -0
  153. mirascope/llm/providers/openai/completions/__init__.py +25 -0
  154. mirascope/llm/{clients → providers}/openai/completions/_utils/__init__.py +2 -0
  155. mirascope/llm/{clients → providers}/openai/completions/_utils/decode.py +60 -6
  156. mirascope/llm/{clients → providers}/openai/completions/_utils/encode.py +37 -26
  157. mirascope/llm/providers/openai/completions/base_provider.py +513 -0
  158. mirascope/llm/providers/openai/completions/provider.py +22 -0
  159. mirascope/llm/providers/openai/model_id.py +31 -0
  160. mirascope/llm/providers/openai/model_info.py +303 -0
  161. mirascope/llm/providers/openai/provider.py +398 -0
  162. mirascope/llm/providers/openai/responses/__init__.py +21 -0
  163. mirascope/llm/{clients → providers}/openai/responses/_utils/decode.py +59 -6
  164. mirascope/llm/{clients → providers}/openai/responses/_utils/encode.py +34 -23
  165. mirascope/llm/providers/openai/responses/provider.py +469 -0
  166. mirascope/llm/providers/provider_id.py +23 -0
  167. mirascope/llm/providers/provider_registry.py +169 -0
  168. mirascope/llm/providers/together/__init__.py +19 -0
  169. mirascope/llm/providers/together/provider.py +40 -0
  170. mirascope/llm/responses/__init__.py +3 -0
  171. mirascope/llm/responses/base_response.py +14 -5
  172. mirascope/llm/responses/base_stream_response.py +35 -6
  173. mirascope/llm/responses/finish_reason.py +1 -0
  174. mirascope/llm/responses/response.py +33 -13
  175. mirascope/llm/responses/root_response.py +12 -13
  176. mirascope/llm/responses/stream_response.py +35 -23
  177. mirascope/llm/responses/usage.py +95 -0
  178. mirascope/llm/tools/__init__.py +9 -2
  179. mirascope/llm/tools/_utils.py +12 -3
  180. mirascope/llm/tools/protocols.py +4 -4
  181. mirascope/llm/tools/tool_schema.py +44 -9
  182. mirascope/llm/tools/tools.py +10 -9
  183. mirascope/ops/__init__.py +156 -0
  184. mirascope/ops/_internal/__init__.py +5 -0
  185. mirascope/ops/_internal/closure.py +1118 -0
  186. mirascope/ops/_internal/configuration.py +126 -0
  187. mirascope/ops/_internal/context.py +76 -0
  188. mirascope/ops/_internal/exporters/__init__.py +26 -0
  189. mirascope/ops/_internal/exporters/exporters.py +342 -0
  190. mirascope/ops/_internal/exporters/processors.py +104 -0
  191. mirascope/ops/_internal/exporters/types.py +165 -0
  192. mirascope/ops/_internal/exporters/utils.py +29 -0
  193. mirascope/ops/_internal/instrumentation/__init__.py +8 -0
  194. mirascope/ops/_internal/instrumentation/llm/__init__.py +8 -0
  195. mirascope/ops/_internal/instrumentation/llm/encode.py +238 -0
  196. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/__init__.py +38 -0
  197. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_input_messages.py +31 -0
  198. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_output_messages.py +38 -0
  199. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_system_instructions.py +18 -0
  200. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/shared.py +100 -0
  201. mirascope/ops/_internal/instrumentation/llm/llm.py +1288 -0
  202. mirascope/ops/_internal/propagation.py +198 -0
  203. mirascope/ops/_internal/protocols.py +51 -0
  204. mirascope/ops/_internal/session.py +139 -0
  205. mirascope/ops/_internal/spans.py +232 -0
  206. mirascope/ops/_internal/traced_calls.py +371 -0
  207. mirascope/ops/_internal/traced_functions.py +394 -0
  208. mirascope/ops/_internal/tracing.py +276 -0
  209. mirascope/ops/_internal/types.py +13 -0
  210. mirascope/ops/_internal/utils.py +75 -0
  211. mirascope/ops/_internal/versioned_calls.py +512 -0
  212. mirascope/ops/_internal/versioned_functions.py +346 -0
  213. mirascope/ops/_internal/versioning.py +303 -0
  214. mirascope/ops/exceptions.py +21 -0
  215. {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a4.dist-info}/METADATA +78 -3
  216. mirascope-2.0.0a4.dist-info/RECORD +247 -0
  217. {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a4.dist-info}/WHEEL +1 -1
  218. mirascope/graphs/__init__.py +0 -22
  219. mirascope/graphs/finite_state_machine.py +0 -625
  220. mirascope/llm/agents/__init__.py +0 -15
  221. mirascope/llm/agents/agent.py +0 -97
  222. mirascope/llm/agents/agent_template.py +0 -45
  223. mirascope/llm/agents/decorator.py +0 -176
  224. mirascope/llm/calls/base_call.py +0 -33
  225. mirascope/llm/clients/__init__.py +0 -34
  226. mirascope/llm/clients/anthropic/__init__.py +0 -25
  227. mirascope/llm/clients/anthropic/_utils/encode.py +0 -243
  228. mirascope/llm/clients/anthropic/clients.py +0 -819
  229. mirascope/llm/clients/anthropic/model_ids.py +0 -8
  230. mirascope/llm/clients/google/__init__.py +0 -20
  231. mirascope/llm/clients/google/clients.py +0 -853
  232. mirascope/llm/clients/google/model_ids.py +0 -15
  233. mirascope/llm/clients/openai/__init__.py +0 -25
  234. mirascope/llm/clients/openai/completions/__init__.py +0 -28
  235. mirascope/llm/clients/openai/completions/_utils/model_features.py +0 -81
  236. mirascope/llm/clients/openai/completions/clients.py +0 -833
  237. mirascope/llm/clients/openai/completions/model_ids.py +0 -8
  238. mirascope/llm/clients/openai/responses/__init__.py +0 -26
  239. mirascope/llm/clients/openai/responses/_utils/__init__.py +0 -13
  240. mirascope/llm/clients/openai/responses/_utils/model_features.py +0 -87
  241. mirascope/llm/clients/openai/responses/clients.py +0 -832
  242. mirascope/llm/clients/openai/responses/model_ids.py +0 -8
  243. mirascope/llm/clients/openai/shared/__init__.py +0 -7
  244. mirascope/llm/clients/openai/shared/_utils.py +0 -55
  245. mirascope/llm/clients/providers.py +0 -175
  246. mirascope-2.0.0a2.dist-info/RECORD +0 -102
  247. /mirascope/llm/{clients → providers}/base/kwargs.py +0 -0
  248. /mirascope/llm/{clients → providers}/base/params.py +0 -0
  249. /mirascope/llm/{clients/anthropic → providers/google}/_utils/__init__.py +0 -0
  250. /mirascope/llm/{clients → providers}/google/message.py +0 -0
  251. /mirascope/llm/{clients/google → providers/openai/responses}/_utils/__init__.py +0 -0
  252. {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -1,625 +0,0 @@
1
- """The `FiniteStateMachine` Class Implementation."""
2
-
3
- import inspect
4
- from collections.abc import Callable, Coroutine, Iterator
5
- from contextlib import contextmanager
6
- from contextvars import ContextVar
7
- from dataclasses import dataclass
8
- from functools import wraps
9
- from typing import Any, Generic, ParamSpec, Protocol, overload
10
- from typing_extensions import TypeVar
11
-
12
- NoneType = type(None)
13
- DepsT = TypeVar("DepsT", default=None)
14
-
15
-
16
- @dataclass
17
- class RunContext(Generic[DepsT]):
18
- """The runtime context for the Finite State Machine (FSM).
19
-
20
- This class is used to store the state of the FSM and the data that is passed between
21
- nodes. Consumers of the FSM can update the context as needed to change the state of
22
- the FSM or pass information between nodes.
23
-
24
- At a minimum the `RunContext` should know:
25
- - The necessary dependencies for running the FSM.
26
- - The current state of the FSM.
27
- - The inputs to the current node being run.
28
- - The transition edges of the FSM.
29
- - Data that is shared between nodes.
30
- - The state history of the FSM execution.
31
- - Error states and fallback handling.
32
- - The queue of incoming requests and their context.
33
-
34
- The last point is important. For the FSM to be efficient, we must enable running
35
- nodes in parallel. This means that we need to be able to queue incoming requests
36
- and their context, and then run the nodes in parallel up to the set thread limit.
37
-
38
- Attributes:
39
- state (...): The current state of the FSM.
40
- inputs (...): The inputs to the current node being run.
41
- transitions (...): The transition edges of the FSM.
42
- data (...): Data that is shared between nodes.
43
- state_history (...): The state history of the FSM execution.
44
- error_states (...): Error states and fallback handling.
45
- """
46
-
47
- deps: DepsT
48
- """The dependencies for the FSM."""
49
-
50
-
51
- FSM_CONTEXT: ContextVar[RunContext[Any] | None] = ContextVar(
52
- "fsm_context", default=None
53
- )
54
-
55
-
56
- class FSMContextError(Exception):
57
- """Raised when a node is executed outside of a context manager."""
58
-
59
- pass
60
-
61
-
62
- NodeDecoratedFunctionP = ParamSpec("NodeDecoratedFunctionP")
63
- NodeDecoratedFunctionR = TypeVar("NodeDecoratedFunctionR", covariant=True)
64
-
65
-
66
- class NodeDecoratedFunction(
67
- Protocol[NodeDecoratedFunctionP, NodeDecoratedFunctionR, DepsT]
68
- ):
69
- """The protocol for functions decorated with the `node` decorator.
70
-
71
- This protocol enables enforcing that the first argument of any `node` decorated
72
- function must be `ctx: RunContext` to ensure that the function can access the global
73
- context of the FSM.
74
- """
75
-
76
- __name__: str
77
-
78
- def __call__(
79
- self,
80
- ctx: RunContext[DepsT],
81
- *args: NodeDecoratedFunctionP.args,
82
- **kwargs: NodeDecoratedFunctionP.kwargs,
83
- ) -> NodeDecoratedFunctionR: ...
84
-
85
-
86
- NodeDecoratorP = ParamSpec("NodeDecoratorP")
87
- NodeDecoratorR = TypeVar("NodeDecoratorR")
88
-
89
-
90
- class NodeDecorator(Protocol[DepsT]):
91
- """The `node` decorator protocol.
92
-
93
- This protocol enables overloading the `node` function such that it can detect and
94
- type async functions correctly. The `node` decorator is used to register a function
95
- as a node in the FSM, so the resulting function when run uncompiled should run just
96
- as the original function would (and thus required matching type hints).
97
- """
98
-
99
- @overload
100
- def __call__(
101
- self,
102
- fn: NodeDecoratedFunction[
103
- NodeDecoratorP, Coroutine[Any, Any, NodeDecoratorR], DepsT
104
- ],
105
- ) -> Callable[NodeDecoratorP, Coroutine[Any, Any, NodeDecoratorR]]: ...
106
-
107
- @overload
108
- def __call__(
109
- self, fn: NodeDecoratedFunction[NodeDecoratorP, NodeDecoratorR, DepsT]
110
- ) -> Callable[NodeDecoratorP, NodeDecoratorR]: ...
111
-
112
- def __call__(
113
- self,
114
- fn: NodeDecoratedFunction[
115
- NodeDecoratorP,
116
- NodeDecoratorR | Coroutine[Any, Any, NodeDecoratorR],
117
- DepsT,
118
- ],
119
- ) -> Callable[
120
- NodeDecoratorP, NodeDecoratorR | Coroutine[Any, Any, NodeDecoratorR]
121
- ]: ...
122
-
123
-
124
- _P = ParamSpec("_P")
125
- _R = TypeVar("_R")
126
-
127
-
128
- @dataclass(kw_only=True)
129
- class FiniteStateMachine(Generic[DepsT]):
130
- '''Automatical Finite State Machine (FSM) compiled from the code execution graph.
131
-
132
- This class provides a global `RunContext` and a `node` decorator that gives function
133
- it decorates access to the global context. The context is used to store the state
134
- of the FSM and the data that is passed between nodes. This means that the consumer
135
- of this class can update the context as needed to change the state of the FSM or
136
- pass information between nodes.
137
-
138
- Let's consider a very simple example of a FSM that increments a given value until it
139
- is divisible by a given divisor. The FSM has two nodes: `increment` and
140
- `divisible_by`. The `increment` node increments the given value by 1, and the
141
- `divisible_by` node checks if the value is divisible by the given divisor. If it is,
142
- the FSM returns the number of increments, otherwise it increments and divides again.
143
-
144
- The FSM might look something like this:
145
-
146
- +-----------+ +-----------+
147
- | increment | ----> | divide | <---- start value
148
- | | <---- | | ----> number of increments
149
- +-----------+ +-----------+
150
-
151
- The FSM has two nodes:
152
- - `increment`: Increments the given value by 1.
153
- - `divisible_by`: Checks if the value is divisible by the given divisor.
154
-
155
- Now, it would be fairly straightforward to implement this FSM by defining the nodes
156
- and the edges between them. However, the FSM can get very complex very quickly, and
157
- it would be nice to have a way to automatically compile the FSM from the code so
158
- that we can build arbitrarily complex systems without having to worry how to
159
- structure them. Once compiled, the code can be run as a finite state machine.
160
-
161
- What if we could just define the code?
162
-
163
- Example:
164
-
165
- ```python
166
- from dataclasses import dataclass
167
-
168
- @dataclass
169
- class Deps:
170
- operations: int = 0
171
-
172
- fsm = FiniteStateMachine(deps_type=Deps)
173
-
174
- @fsm.node()
175
- async def increment(ctx: RunContext[Deps], value: int) -> int:
176
- ctx.deps.operations += 1
177
- return value + 1
178
-
179
- @fsm.node()
180
- async def divide(ctx: RunContext[Deps], value: int, divisor: int) -> int:
181
- ctx.deps.operations += 1
182
- return value // divisor
183
-
184
- @fsm.node()
185
- async def reach_zero(ctx: RunContext[Deps], value: int, divisor: int) -> int:
186
- while value != 0:
187
- value = await increment(value)
188
- value = await divide(value, divisor)
189
- return ctx.deps.operations
190
-
191
- # in async running loop
192
- async with fsm.context(deps=Deps(0)) as ctx:
193
- operations = await reach_zero(5, 3)
194
- print(f"Operations: {operations}")
195
- ```
196
-
197
- What about an FSM that represents a librarian?
198
-
199
- Example:
200
-
201
- ```python
202
- import asyncio
203
- from dataclasses import dataclass
204
-
205
- from mirascope import BaseMessageParam, Messages, llm
206
- from mirascope import graphs as g
207
-
208
-
209
- @dataclass
210
- class Book
211
- title: str
212
- author: str
213
-
214
-
215
- @dataclass
216
- class Library:
217
- librarian: str
218
- messages: list[BaseMessageParam]
219
- all_books: list[Book]
220
- available_books: dict[str, bool]
221
-
222
-
223
- machine = g.FiniteStateMachine(deps_type=Library)
224
-
225
-
226
- @machine.node()
227
- @llm.tool
228
- def all_books(ctx: g.RunContext[Library]) -> list[Book]:
229
- """Returns the titles of all books the library owns."""
230
- return ctx.deps.all_books
231
-
232
-
233
- @machine.node()
234
- @llm.tool
235
- def book_is_available(ctx: g.RunContext[Library], title: str) -> str:
236
- """Returns the author of the book with the given title."""
237
- return ctx.deps.available_books[title]
238
-
239
-
240
- @machine.node()
241
- @llm.call(provider="openai:completions", model_id="gpt-4o-mini", tools=[all_books, book_is_available])
242
- async def answer_question(ctx: g.RunContext[Library]) -> str:
243
- return f"You are a librarian named {ctx.deps.librarian}"
244
-
245
-
246
- @machine.node()
247
- async def handle_tools(ctx: g.RunContext[Library], tools: list[Tool]) -> None:
248
- tool_tasks = [tool.call() for tool in tools]
249
- tool_outputs = asyncio.gather(*tool_tasks)
250
- tools_and_outputs = [(tool, output) for tool, output in zip(tools, tools_outputs, strict=True)]
251
- ctx.deps.messages += llm.CallResponse.tool_message_params(tools_and_outputs)
252
-
253
- @machine.node()
254
- async def librarian(ctx: g.RunContext[Library], question: str) -> llm.CallResponse:
255
- response = await answer_question(question)
256
- if response.user_message_param:
257
- ctx.deps.messages.append(response.user_message_param)
258
- ctx.deps.messages.append(response.message_param)
259
- if tools := response.tools:
260
- await handle_tools(tools)
261
- return await answer_question("")
262
- return response
263
-
264
-
265
- agent = machine.compile(start=librarian)
266
-
267
- response = agent.run(
268
- "Do you carry The Name of the Wind? Is it available?",
269
- deps=Library(
270
- librarian="Mira",
271
- all_books=[
272
- Book("The Name of the Wind", "Patrick Rothfuss"),
273
- Book("Mistborn: The Final Empire", "Brandon Sanderson"),
274
- ],
275
- available_books={
276
- "The Name of the Wind": True,
277
- "Mistborn: The Final Empire": False,
278
- },
279
- )
280
- )
281
- print(response.content)
282
- ```
283
-
284
- Ultimately each node is fairly self-contained except for `librarian`, which can be
285
- thought of as it's own little sub-FSM or controller. The `librarian` node is the
286
- entry point to the FSM and is responsible for handling the incoming question,
287
- calling the `answer_question` node, and then handling the tools that are returned.
288
- The `answer_question` node is responsible for answering the question, and the
289
- `handle_tools` node is responsible for calling the tools and then returning the
290
- results.
291
-
292
- The compiled graph might look something like this:
293
-
294
- +-----------------+ +-------------------+
295
- question --> | answer_question | ----> | _handle_response_ | --> response
296
- +-----------------+ +-------------------+
297
- ^ |
298
- | v
299
- +-------------------+
300
- | handle_tools |
301
- +-------------------+
302
-
303
-
304
- Inside the `librarian` function, really all we're doing is calling `answer_question`
305
- and then handling the response such that we either call any requested tools and call
306
- `answer_question` again, or we return the response. This is a very simple example,
307
- but it shows how we can build an efficient FSM without having to worry about the
308
- nitty gritty details of how the FSM is structured.
309
-
310
- We get to code. The FSM then builds itself.
311
- '''
312
-
313
- deps_type: type[DepsT]
314
- """The type of the dependencies for the FSM."""
315
-
316
- @contextmanager
317
- def context(self, *, deps: DepsT) -> Iterator[RunContext[DepsT]]:
318
- """Creates a context manager for the FSM that handles both sync and async.
319
-
320
- This method returns an instance of FSMContextManager which can be used with both
321
- `with` and `async with` statements. The context manager sets up a RunContext
322
- that all node-decorated functions will use while the context is active. The
323
- context is stored using contextvars.ContextVar which properly propagates through
324
- async code, allowing context to be maintained across await points and between
325
- nodes so long as each node is called within the context.
326
-
327
- NOTE: This simplified implementation has some limitations with complex nested
328
- async calls across context boundaries. For nested nodes calling other nodes
329
- asynchronously after a context exit, the context may not be properly preserved.
330
- In these cases, using a compiled machine is recommended.
331
-
332
- NOTE: We implement stubs using the sync case because the async case is currently
333
- the same and does not require separate implementations. In the future we may update
334
- this to handle more complex async cases where the context e.g. pulls from an
335
- external source or something.
336
-
337
- Example:
338
-
339
- Synchronous usage:
340
- ```python
341
- with fsm.context(deps=Deps()) as ctx:
342
- result = increment(5) # ctx is automatically passed
343
- ```
344
-
345
- Example:
346
-
347
- Asynchronous usage:
348
- ```python
349
- async with fsm.context(deps=Deps()) as ctx:
350
- result = await async_increment(5) # ctx is automatically passed
351
- ```
352
-
353
- Args:
354
- deps: The dependencies for the FSM.
355
-
356
- Returns:
357
- An FSMContextManager instance that supports both `with` and `async with`.
358
- """
359
- context = RunContext(deps=deps)
360
- token = FSM_CONTEXT.set(context)
361
- try:
362
- yield context
363
- finally:
364
- FSM_CONTEXT.reset(token)
365
-
366
- def node(self) -> NodeDecorator[DepsT]:
367
- """Registers the given function as a node in the FSM.
368
-
369
- This decorator transforms a function that requires a RunContext as its first
370
- parameter into a function that can be called without explicitly passing the
371
- context. The context is retrieved from contextvars.ContextVar which properly
372
- propagates through async code, ensuring that context is maintained across
373
- await points.
374
-
375
- This means that when a node calls another node, even asynchronously, the context
376
- is automatically propagated without any special handling. This allows for
377
- building complex node graphs with proper context flow.
378
-
379
- If the decorated function is called outside of a context manager, a
380
- FSMContextError will be raised.
381
-
382
- Returns:
383
- A decorator that registers the given function as a node in the FSM.
384
- """
385
-
386
- @overload
387
- def decorator(
388
- fn: NodeDecoratedFunction[_P, Coroutine[Any, Any, _R], DepsT],
389
- ) -> Callable[_P, Coroutine[Any, Any, _R]]: ...
390
-
391
- @overload
392
- def decorator(
393
- fn: NodeDecoratedFunction[_P, _R, DepsT],
394
- ) -> Callable[_P, _R]: ...
395
-
396
- def decorator(
397
- fn: NodeDecoratedFunction[_P, _R | Coroutine[Any, Any, _R], DepsT],
398
- ) -> Callable[_P, _R | Coroutine[Any, Any, _R]]:
399
- @wraps(fn)
400
- def context_wrapper(
401
- *args: _P.args, **kwargs: _P.kwargs
402
- ) -> _R | Coroutine[Any, Any, _R]:
403
- context = FSM_CONTEXT.get()
404
- if context is None:
405
- raise FSMContextError(
406
- f"Node `{fn.__name__}` called outside of a context manager. "
407
- f"Use 'with fsm.context(deps=...)' to provide a context."
408
- )
409
-
410
- if inspect.iscoroutinefunction(fn):
411
- # For async functions, just create and return a coroutine with the
412
- # context management inside so we handle contextvars correctly
413
- @wraps(fn)
414
- async def inner_async() -> _R:
415
- return await fn(context, *args, **kwargs) # pyright: ignore [reportGeneralTypeIssues]
416
-
417
- return inner_async()
418
-
419
- return fn(context, *args, **kwargs)
420
-
421
- return context_wrapper
422
-
423
- return decorator
424
-
425
- def compile(
426
- self,
427
- start: Callable[_P, _R],
428
- *,
429
- deps: DepsT = None,
430
- ) -> "CompiledMachine[_P, _R, DepsT]":
431
- """Returns the compiled FSM.
432
-
433
- Compiles the FSM and returns a CompiledMachine that can be used to run the FSM.
434
- The CompiledMachine will use the provided start function as the entry point to
435
- the FSM and will create a RunContext with the provided dependencies.
436
-
437
- Args:
438
- start: The entry point to the FSM.
439
- deps: The dependencies for the FSM.
440
-
441
- Returns:
442
- A CompiledMachine that can be used to run the FSM.
443
- """
444
- return CompiledMachine(fsm=self, start=start, deps=deps)
445
-
446
-
447
- class CompiledMachine(Generic[_P, _R, DepsT]):
448
- """The compiled Finite State Machine (FSM).
449
-
450
- This class represents a compiled FSM that can be run with a specific start function.
451
- The FSM will use the provided start function as the entry point and will create a
452
- RunContext with the provided dependencies when run.
453
-
454
- Attributes:
455
- fsm: The FiniteStateMachine that was compiled.
456
- start: The entry point to the FSM.
457
- deps: The dependencies for the FSM.
458
- """
459
-
460
- fsm: FiniteStateMachine[DepsT]
461
- start: Callable[_P, _R]
462
- deps: DepsT
463
-
464
- def __init__(
465
- self,
466
- *,
467
- fsm: FiniteStateMachine[DepsT],
468
- start: Callable[_P, _R],
469
- deps: DepsT = None,
470
- ) -> None:
471
- """Initializes an instance of `CompiledMachine`."""
472
- self.fsm = fsm
473
- self.start = start
474
- self.deps = deps
475
-
476
- def run(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
477
- """Runs the FSM synchronously.
478
-
479
- This method runs the FSM using the specified start function and dependencies.
480
- It sets up a context manager to provide the RunContext for all node-decorated
481
- functions called during the execution of the FSM.
482
-
483
- This method should only be used with synchronous start functions. For async
484
- functions, use `run_async` instead.
485
-
486
- Args:
487
- *args: The positional arguments to pass to the start function.
488
- **kwargs: The keyword arguments to pass to the start function.
489
-
490
- Returns:
491
- The result of running the FSM.
492
-
493
- Raises:
494
- FSMContextError: If the start function or any node-decorated function is
495
- called outside of a context manager.
496
- RuntimeError: If called with an async function as the start function.
497
- """
498
- if inspect.iscoroutinefunction(self.start):
499
- raise RuntimeError(
500
- f"Cannot use run() with async function {self.start.__name__}. "
501
- f"Use run_async() instead."
502
- )
503
-
504
- with self.fsm.context(deps=self.deps):
505
- return self.start(*args, **kwargs)
506
-
507
- async def run_async(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
508
- """Runs the FSM asynchronously.
509
-
510
- This method runs the FSM using the specified start function and dependencies.
511
- It sets up an async context manager to provide the RunContext for all node-decorated
512
- functions called during the execution of the FSM.
513
-
514
- This method should only be used with asynchronous start functions. For synchronous
515
- functions, use `run` instead.
516
-
517
- Args:
518
- *args: The positional arguments to pass to the start function.
519
- **kwargs: The keyword arguments to pass to the start function.
520
-
521
- Returns:
522
- The result of running the FSM.
523
-
524
- Raises:
525
- FSMContextError: If the start function or any node-decorated function is
526
- called outside of a context manager.
527
- RuntimeError: If called with a sync function as the start function.
528
- """
529
- with self.fsm.context(deps=self.deps):
530
- result = self.start(*args, **kwargs)
531
- if inspect.isawaitable(result):
532
- return await result
533
- return result
534
-
535
-
536
- async def main() -> None:
537
- from dataclasses import dataclass
538
-
539
- @dataclass
540
- class Deps:
541
- operations: int = 0
542
-
543
- fsm = FiniteStateMachine(deps_type=Deps)
544
-
545
- # Example 1: Using the FSM with synchronous functions
546
- print("Example 1: Using the FSM with synchronous functions")
547
-
548
- @fsm.node()
549
- def increment(ctx: RunContext[Deps], value: int) -> int:
550
- ctx.deps.operations += 1
551
- return value + 1
552
-
553
- @fsm.node()
554
- def divide(ctx: RunContext[Deps], value: int, divisor: int) -> int:
555
- ctx.deps.operations += 1
556
- return value // divisor
557
-
558
- @fsm.node()
559
- def reach_zero(ctx: RunContext[Deps], value: int, divisor: int) -> int:
560
- while value != 0:
561
- value = increment(value)
562
- value = divide(value, divisor)
563
- return ctx.deps.operations
564
-
565
- with fsm.context(deps=Deps(0)):
566
- operations = reach_zero(5, 3)
567
- print(f"Operations: {operations}")
568
-
569
- # Example 2: Run the compiled sync machine
570
- print("\nExample 2: Run the compiled sync machine")
571
- machine = fsm.compile(start=reach_zero, deps=Deps(0))
572
- num_operations = machine.run(value=5, divisor=3)
573
- print(f"Compiled sync operations: {num_operations}")
574
-
575
- # # Define async nodes
576
- @fsm.node()
577
- async def async_increment(ctx: RunContext[Deps], value: int) -> int:
578
- ctx.deps.operations += 1
579
- return value + 1
580
-
581
- @fsm.node()
582
- async def async_divide(ctx: RunContext[Deps], value: int, divisor: int) -> int:
583
- ctx.deps.operations += 1
584
- return value // divisor
585
-
586
- @fsm.node()
587
- async def async_reach_zero(ctx: RunContext[Deps], value: int, divisor: int) -> int:
588
- while value != 0:
589
- value = await async_increment(value)
590
- value = await async_divide(value, divisor)
591
- return ctx.deps.operations
592
-
593
- # Example 3: Using synchronous context with async functions within boundaries
594
- print("\nExample 3: Synchronous context with async functions")
595
- with fsm.context(deps=Deps(0)):
596
- operations = await async_reach_zero(5, 3)
597
- print(f"Async operations in sync context: {operations}")
598
-
599
- # Example 3.5: Using asynchronous context with async functions within boundaries
600
- print("\nExample 3.5: Asynchronous context with async functions")
601
- with fsm.context(deps=Deps(0)):
602
- operations = await async_reach_zero(5, 3)
603
- print(f"Async operations in async context: {operations}")
604
-
605
- # Example 4: Demonstrate context preservation across await boundaries
606
- print("\nContext preservation example:")
607
- with fsm.context(deps=Deps(0)):
608
- # Create the coroutine task inside the context
609
- operations_task = async_reach_zero(5, 3)
610
- print("Task created inside context manager")
611
- # But await it outside the context - this will work because we preserve the context
612
- operations = await operations_task
613
- print(f"Task awaited outside context, operations: {operations}")
614
-
615
- # Example 5: Run the compiled async machine
616
- print("\nCompiled async machine example:")
617
- async_machine = fsm.compile(start=async_reach_zero, deps=Deps(0))
618
- async_num_operations = await async_machine.run_async(value=5, divisor=3)
619
- print(f"Compiled async operations: {async_num_operations}")
620
-
621
-
622
- if __name__ == "__main__":
623
- import asyncio
624
-
625
- asyncio.run(main())
@@ -1,15 +0,0 @@
1
- """The Agents module for creating and managing LLM agents."""
2
-
3
- from .agent import Agent, AsyncAgent, BaseAgent
4
- from .agent_template import AgentTemplate, AsyncAgentTemplate
5
- from .decorator import AgentDecorator, agent
6
-
7
- __all__ = [
8
- "Agent",
9
- "AgentDecorator",
10
- "AgentTemplate",
11
- "AsyncAgent",
12
- "AsyncAgentTemplate",
13
- "BaseAgent",
14
- "agent",
15
- ]