fast-agent-mcp 0.0.7__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.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (100) hide show
  1. fast_agent_mcp-0.0.7.dist-info/METADATA +322 -0
  2. fast_agent_mcp-0.0.7.dist-info/RECORD +100 -0
  3. fast_agent_mcp-0.0.7.dist-info/WHEEL +4 -0
  4. fast_agent_mcp-0.0.7.dist-info/entry_points.txt +5 -0
  5. fast_agent_mcp-0.0.7.dist-info/licenses/LICENSE +201 -0
  6. mcp_agent/__init__.py +0 -0
  7. mcp_agent/agents/__init__.py +0 -0
  8. mcp_agent/agents/agent.py +277 -0
  9. mcp_agent/app.py +303 -0
  10. mcp_agent/cli/__init__.py +0 -0
  11. mcp_agent/cli/__main__.py +4 -0
  12. mcp_agent/cli/commands/bootstrap.py +221 -0
  13. mcp_agent/cli/commands/config.py +11 -0
  14. mcp_agent/cli/commands/setup.py +229 -0
  15. mcp_agent/cli/main.py +68 -0
  16. mcp_agent/cli/terminal.py +24 -0
  17. mcp_agent/config.py +334 -0
  18. mcp_agent/console.py +28 -0
  19. mcp_agent/context.py +251 -0
  20. mcp_agent/context_dependent.py +48 -0
  21. mcp_agent/core/fastagent.py +1013 -0
  22. mcp_agent/eval/__init__.py +0 -0
  23. mcp_agent/event_progress.py +88 -0
  24. mcp_agent/executor/__init__.py +0 -0
  25. mcp_agent/executor/decorator_registry.py +120 -0
  26. mcp_agent/executor/executor.py +293 -0
  27. mcp_agent/executor/task_registry.py +34 -0
  28. mcp_agent/executor/temporal.py +405 -0
  29. mcp_agent/executor/workflow.py +197 -0
  30. mcp_agent/executor/workflow_signal.py +325 -0
  31. mcp_agent/human_input/__init__.py +0 -0
  32. mcp_agent/human_input/handler.py +49 -0
  33. mcp_agent/human_input/types.py +58 -0
  34. mcp_agent/logging/__init__.py +0 -0
  35. mcp_agent/logging/events.py +123 -0
  36. mcp_agent/logging/json_serializer.py +163 -0
  37. mcp_agent/logging/listeners.py +216 -0
  38. mcp_agent/logging/logger.py +365 -0
  39. mcp_agent/logging/rich_progress.py +120 -0
  40. mcp_agent/logging/tracing.py +140 -0
  41. mcp_agent/logging/transport.py +461 -0
  42. mcp_agent/mcp/__init__.py +0 -0
  43. mcp_agent/mcp/gen_client.py +85 -0
  44. mcp_agent/mcp/mcp_activity.py +18 -0
  45. mcp_agent/mcp/mcp_agent_client_session.py +242 -0
  46. mcp_agent/mcp/mcp_agent_server.py +56 -0
  47. mcp_agent/mcp/mcp_aggregator.py +394 -0
  48. mcp_agent/mcp/mcp_connection_manager.py +330 -0
  49. mcp_agent/mcp/stdio.py +104 -0
  50. mcp_agent/mcp_server_registry.py +275 -0
  51. mcp_agent/progress_display.py +10 -0
  52. mcp_agent/resources/examples/decorator/main.py +26 -0
  53. mcp_agent/resources/examples/decorator/optimizer.py +78 -0
  54. mcp_agent/resources/examples/decorator/orchestrator.py +68 -0
  55. mcp_agent/resources/examples/decorator/parallel.py +81 -0
  56. mcp_agent/resources/examples/decorator/router.py +56 -0
  57. mcp_agent/resources/examples/decorator/tiny.py +22 -0
  58. mcp_agent/resources/examples/mcp_researcher/main-evalopt.py +53 -0
  59. mcp_agent/resources/examples/mcp_researcher/main.py +38 -0
  60. mcp_agent/telemetry/__init__.py +0 -0
  61. mcp_agent/telemetry/usage_tracking.py +18 -0
  62. mcp_agent/workflows/__init__.py +0 -0
  63. mcp_agent/workflows/embedding/__init__.py +0 -0
  64. mcp_agent/workflows/embedding/embedding_base.py +61 -0
  65. mcp_agent/workflows/embedding/embedding_cohere.py +49 -0
  66. mcp_agent/workflows/embedding/embedding_openai.py +46 -0
  67. mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
  68. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +359 -0
  69. mcp_agent/workflows/intent_classifier/__init__.py +0 -0
  70. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +120 -0
  71. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +134 -0
  72. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +45 -0
  73. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +45 -0
  74. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +161 -0
  75. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +60 -0
  76. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +60 -0
  77. mcp_agent/workflows/llm/__init__.py +0 -0
  78. mcp_agent/workflows/llm/augmented_llm.py +645 -0
  79. mcp_agent/workflows/llm/augmented_llm_anthropic.py +539 -0
  80. mcp_agent/workflows/llm/augmented_llm_openai.py +615 -0
  81. mcp_agent/workflows/llm/llm_selector.py +345 -0
  82. mcp_agent/workflows/llm/model_factory.py +175 -0
  83. mcp_agent/workflows/orchestrator/__init__.py +0 -0
  84. mcp_agent/workflows/orchestrator/orchestrator.py +407 -0
  85. mcp_agent/workflows/orchestrator/orchestrator_models.py +154 -0
  86. mcp_agent/workflows/orchestrator/orchestrator_prompts.py +113 -0
  87. mcp_agent/workflows/parallel/__init__.py +0 -0
  88. mcp_agent/workflows/parallel/fan_in.py +350 -0
  89. mcp_agent/workflows/parallel/fan_out.py +187 -0
  90. mcp_agent/workflows/parallel/parallel_llm.py +141 -0
  91. mcp_agent/workflows/router/__init__.py +0 -0
  92. mcp_agent/workflows/router/router_base.py +276 -0
  93. mcp_agent/workflows/router/router_embedding.py +240 -0
  94. mcp_agent/workflows/router/router_embedding_cohere.py +59 -0
  95. mcp_agent/workflows/router/router_embedding_openai.py +59 -0
  96. mcp_agent/workflows/router/router_llm.py +301 -0
  97. mcp_agent/workflows/swarm/__init__.py +0 -0
  98. mcp_agent/workflows/swarm/swarm.py +320 -0
  99. mcp_agent/workflows/swarm/swarm_anthropic.py +42 -0
  100. mcp_agent/workflows/swarm/swarm_openai.py +41 -0
@@ -0,0 +1,405 @@
1
+ """
2
+ Temporal based orchestrator for the MCP Agent.
3
+ Temporal provides durable execution and robust workflow orchestration,
4
+ as well as dynamic control flow, making it a good choice for an AI agent orchestrator.
5
+ Read more: https://docs.temporal.io/develop/python/core-application
6
+ """
7
+
8
+ import asyncio
9
+ import functools
10
+ import uuid
11
+ from typing import (
12
+ Any,
13
+ AsyncIterator,
14
+ Callable,
15
+ Coroutine,
16
+ Dict,
17
+ List,
18
+ Optional,
19
+ TYPE_CHECKING,
20
+ )
21
+
22
+ from pydantic import ConfigDict
23
+ from temporalio import activity, workflow, exceptions
24
+ from temporalio.client import Client as TemporalClient
25
+ from temporalio.worker import Worker
26
+
27
+ from mcp_agent.config import TemporalSettings
28
+ from mcp_agent.executor.executor import Executor, ExecutorConfig, R
29
+ from mcp_agent.executor.workflow_signal import (
30
+ BaseSignalHandler,
31
+ Signal,
32
+ SignalHandler,
33
+ SignalRegistration,
34
+ SignalValueT,
35
+ )
36
+
37
+ if TYPE_CHECKING:
38
+ from mcp_agent.context import Context
39
+
40
+
41
+ class TemporalSignalHandler(BaseSignalHandler[SignalValueT]):
42
+ """Temporal-based signal handling using workflow signals"""
43
+
44
+ async def wait_for_signal(self, signal, timeout_seconds=None) -> SignalValueT:
45
+ if not workflow._Runtime.current():
46
+ raise RuntimeError(
47
+ "TemporalSignalHandler.wait_for_signal must be called from within a workflow"
48
+ )
49
+
50
+ unique_signal_name = f"{signal.name}_{uuid.uuid4()}"
51
+ registration = SignalRegistration(
52
+ signal_name=signal.name,
53
+ unique_name=unique_signal_name,
54
+ workflow_id=workflow.info().workflow_id,
55
+ )
56
+
57
+ # Container for signal value
58
+ container = {"value": None, "completed": False}
59
+
60
+ # Define the signal handler for this specific registration
61
+ @workflow.signal(name=unique_signal_name)
62
+ def signal_handler(value: SignalValueT):
63
+ container["value"] = value
64
+ container["completed"] = True
65
+
66
+ async with self._lock:
67
+ # Register both the signal registration and handler atomically
68
+ self._pending_signals.setdefault(signal.name, []).append(registration)
69
+ self._handlers.setdefault(signal.name, []).append(
70
+ (unique_signal_name, signal_handler)
71
+ )
72
+
73
+ try:
74
+ # Wait for signal with optional timeout
75
+ await workflow.wait_condition(
76
+ lambda: container["completed"], timeout=timeout_seconds
77
+ )
78
+
79
+ return container["value"]
80
+ except asyncio.TimeoutError as exc:
81
+ raise TimeoutError(f"Timeout waiting for signal {signal.name}") from exc
82
+ finally:
83
+ async with self._lock:
84
+ # Remove ourselves from _pending_signals
85
+ if signal.name in self._pending_signals:
86
+ self._pending_signals[signal.name] = [
87
+ sr
88
+ for sr in self._pending_signals[signal.name]
89
+ if sr.unique_name != unique_signal_name
90
+ ]
91
+ if not self._pending_signals[signal.name]:
92
+ del self._pending_signals[signal.name]
93
+
94
+ # Remove ourselves from _handlers
95
+ if signal.name in self._handlers:
96
+ self._handlers[signal.name] = [
97
+ h
98
+ for h in self._handlers[signal.name]
99
+ if h[0] != unique_signal_name
100
+ ]
101
+ if not self._handlers[signal.name]:
102
+ del self._handlers[signal.name]
103
+
104
+ def on_signal(self, signal_name):
105
+ """Decorator to register a signal handler."""
106
+
107
+ def decorator(func: Callable) -> Callable:
108
+ # Create unique signal name for this handler
109
+ unique_signal_name = f"{signal_name}_{uuid.uuid4()}"
110
+
111
+ # Create the actual handler that will be registered with Temporal
112
+ @workflow.signal(name=unique_signal_name)
113
+ async def wrapped(signal_value: SignalValueT):
114
+ # Create a signal object to pass to the handler
115
+ signal = Signal(
116
+ name=signal_name,
117
+ payload=signal_value,
118
+ workflow_id=workflow.info().workflow_id,
119
+ )
120
+ if asyncio.iscoroutinefunction(func):
121
+ await func(signal)
122
+ else:
123
+ func(signal)
124
+
125
+ # Register the handler under the original signal name
126
+ self._handlers.setdefault(signal_name, []).append(
127
+ (unique_signal_name, wrapped)
128
+ )
129
+ return func
130
+
131
+ return decorator
132
+
133
+ async def signal(self, signal):
134
+ self.validate_signal(signal)
135
+
136
+ workflow_handle = workflow.get_external_workflow_handle(
137
+ workflow_id=signal.workflow_id
138
+ )
139
+
140
+ # Send the signal to all registrations of this signal
141
+ async with self._lock:
142
+ signal_tasks = []
143
+
144
+ if signal.name in self._pending_signals:
145
+ for pending_signal in self._pending_signals[signal.name]:
146
+ registration = pending_signal.registration
147
+ if registration.workflow_id == signal.workflow_id:
148
+ # Only signal for registrations of that workflow
149
+ signal_tasks.append(
150
+ workflow_handle.signal(
151
+ registration.unique_name, signal.payload
152
+ )
153
+ )
154
+ else:
155
+ continue
156
+
157
+ # Notify any registered handler functions
158
+ if signal.name in self._handlers:
159
+ for unique_name, _ in self._handlers[signal.name]:
160
+ signal_tasks.append(
161
+ workflow_handle.signal(unique_name, signal.payload)
162
+ )
163
+
164
+ await asyncio.gather(*signal_tasks, return_exceptions=True)
165
+
166
+ def validate_signal(self, signal):
167
+ super().validate_signal(signal)
168
+ # Add TemporalSignalHandler-specific validation
169
+ if signal.workflow_id is None:
170
+ raise ValueError(
171
+ "No workflow_id provided on Signal. That is required for Temporal signals"
172
+ )
173
+
174
+
175
+ class TemporalExecutorConfig(ExecutorConfig, TemporalSettings):
176
+ """Configuration for Temporal executors."""
177
+
178
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
179
+
180
+
181
+ class TemporalExecutor(Executor):
182
+ """Executor that runs @workflows as Temporal workflows, with @workflow_tasks as Temporal activities"""
183
+
184
+ def __init__(
185
+ self,
186
+ config: TemporalExecutorConfig | None = None,
187
+ signal_bus: SignalHandler | None = None,
188
+ client: TemporalClient | None = None,
189
+ context: Optional["Context"] = None,
190
+ **kwargs,
191
+ ):
192
+ signal_bus = signal_bus or TemporalSignalHandler()
193
+ super().__init__(
194
+ engine="temporal",
195
+ config=config,
196
+ signal_bus=signal_bus,
197
+ context=context,
198
+ **kwargs,
199
+ )
200
+ self.config: TemporalExecutorConfig = (
201
+ config or self.context.config.temporal or TemporalExecutorConfig()
202
+ )
203
+ self.client = client
204
+ self._worker = None
205
+ self._activity_semaphore = None
206
+
207
+ if config.max_concurrent_activities is not None:
208
+ self._activity_semaphore = asyncio.Semaphore(
209
+ self.config.max_concurrent_activities
210
+ )
211
+
212
+ @staticmethod
213
+ def wrap_as_activity(
214
+ activity_name: str,
215
+ func: Callable[..., R] | Coroutine[Any, Any, R],
216
+ **kwargs: Any,
217
+ ) -> Coroutine[Any, Any, R]:
218
+ """
219
+ Convert a function into a Temporal activity and return its info.
220
+ """
221
+
222
+ @activity.defn(name=activity_name)
223
+ async def wrapped_activity(*args, **local_kwargs):
224
+ try:
225
+ if asyncio.iscoroutinefunction(func):
226
+ return await func(*args, **local_kwargs)
227
+ elif asyncio.iscoroutine(func):
228
+ return await func
229
+ else:
230
+ return func(*args, **local_kwargs)
231
+ except Exception as e:
232
+ # Handle exceptions gracefully
233
+ raise e
234
+
235
+ return wrapped_activity
236
+
237
+ async def _execute_task_as_async(
238
+ self, task: Callable[..., R] | Coroutine[Any, Any, R], **kwargs: Any
239
+ ) -> R | BaseException:
240
+ async def run_task(task: Callable[..., R] | Coroutine[Any, Any, R]) -> R:
241
+ try:
242
+ if asyncio.iscoroutine(task):
243
+ return await task
244
+ elif asyncio.iscoroutinefunction(task):
245
+ return await task(**kwargs)
246
+ else:
247
+ # Execute the callable and await if it returns a coroutine
248
+ loop = asyncio.get_running_loop()
249
+
250
+ # If kwargs are provided, wrap the function with partial
251
+ if kwargs:
252
+ wrapped_task = functools.partial(task, **kwargs)
253
+ result = await loop.run_in_executor(None, wrapped_task)
254
+ else:
255
+ result = await loop.run_in_executor(None, task)
256
+
257
+ # Handle case where the sync function returns a coroutine
258
+ if asyncio.iscoroutine(result):
259
+ return await result
260
+
261
+ return result
262
+ except Exception as e:
263
+ # TODO: saqadri - adding logging or other error handling here
264
+ return e
265
+
266
+ if self._activity_semaphore:
267
+ async with self._activity_semaphore:
268
+ return await run_task(task)
269
+ else:
270
+ return await run_task(task)
271
+
272
+ async def _execute_task(
273
+ self, task: Callable[..., R] | Coroutine[Any, Any, R], **kwargs: Any
274
+ ) -> R | BaseException:
275
+ func = task.func if isinstance(task, functools.partial) else task
276
+ is_workflow_task = getattr(func, "is_workflow_task", False)
277
+ if not is_workflow_task:
278
+ return await asyncio.create_task(
279
+ self._execute_task_as_async(task, **kwargs)
280
+ )
281
+
282
+ execution_metadata: Dict[str, Any] = getattr(func, "execution_metadata", {})
283
+
284
+ # Derive stable activity name, e.g. module + qualname
285
+ activity_name = execution_metadata.get("activity_name")
286
+ if not activity_name:
287
+ activity_name = f"{func.__module__}.{func.__qualname__}"
288
+
289
+ schedule_to_close = execution_metadata.get(
290
+ "schedule_to_close_timeout", self.config.timeout_seconds
291
+ )
292
+
293
+ retry_policy = execution_metadata.get("retry_policy", None)
294
+
295
+ _task_activity = self.wrap_as_activity(activity_name=activity_name, func=task)
296
+
297
+ # # For partials, we pass the partial's arguments
298
+ # args = task.args if isinstance(task, functools.partial) else ()
299
+ try:
300
+ result = await workflow.execute_activity(
301
+ activity_name,
302
+ args=kwargs.get("args", ()),
303
+ task_queue=self.config.task_queue,
304
+ schedule_to_close_timeout=schedule_to_close,
305
+ retry_policy=retry_policy,
306
+ **kwargs,
307
+ )
308
+ return result
309
+ except Exception as e:
310
+ # Properly propagate activity errors
311
+ if isinstance(e, exceptions.ActivityError):
312
+ raise e.cause if e.cause else e
313
+ raise
314
+
315
+ async def execute(
316
+ self,
317
+ *tasks: Callable[..., R] | Coroutine[Any, Any, R],
318
+ **kwargs: Any,
319
+ ) -> List[R | BaseException]:
320
+ # Must be called from within a workflow
321
+ if not workflow._Runtime.current():
322
+ raise RuntimeError(
323
+ "TemporalExecutor.execute must be called from within a workflow"
324
+ )
325
+
326
+ # TODO: saqadri - validate if async with self.execution_context() is needed here
327
+ async with self.execution_context():
328
+ return await asyncio.gather(
329
+ *(self._execute_task(task, **kwargs) for task in tasks),
330
+ return_exceptions=True,
331
+ )
332
+
333
+ async def execute_streaming(
334
+ self,
335
+ *tasks: Callable[..., R] | Coroutine[Any, Any, R],
336
+ **kwargs: Any,
337
+ ) -> AsyncIterator[R | BaseException]:
338
+ if not workflow._Runtime.current():
339
+ raise RuntimeError(
340
+ "TemporalExecutor.execute_streaming must be called from within a workflow"
341
+ )
342
+
343
+ # TODO: saqadri - validate if async with self.execution_context() is needed here
344
+ async with self.execution_context():
345
+ # Create futures for all tasks
346
+ futures = [self._execute_task(task, **kwargs) for task in tasks]
347
+ pending = set(futures)
348
+
349
+ while pending:
350
+ done, pending = await workflow.wait(
351
+ pending, return_when=asyncio.FIRST_COMPLETED
352
+ )
353
+ for future in done:
354
+ try:
355
+ result = await future
356
+ yield result
357
+ except Exception as e:
358
+ yield e
359
+
360
+ async def ensure_client(self):
361
+ """Ensure we have a connected Temporal client."""
362
+ if self.client is None:
363
+ self.client = await TemporalClient.connect(
364
+ target_host=self.config.host,
365
+ namespace=self.config.namespace,
366
+ api_key=self.config.api_key,
367
+ )
368
+
369
+ return self.client
370
+
371
+ async def start_worker(self):
372
+ """
373
+ Start a worker in this process, auto-registering all tasks
374
+ from the global registry. Also picks up any classes decorated
375
+ with @workflow_defn as recognized workflows.
376
+ """
377
+ await self.ensure_client()
378
+
379
+ if self._worker is None:
380
+ # We'll collect the activities from the global registry
381
+ # and optionally wrap them with `activity.defn` if we want
382
+ # (Not strictly required if your code calls `execute_activity("name")` by name)
383
+ activity_registry = self.context.task_registry
384
+ activities = []
385
+ for name in activity_registry.list_activities():
386
+ activities.append(activity_registry.get_activity(name))
387
+
388
+ # Now we attempt to discover any classes that are recognized as workflows
389
+ # But in this simple example, we rely on the user specifying them or
390
+ # we might do a dynamic scan.
391
+ # For demonstration, we'll just assume the user is only using
392
+ # the workflow classes they decorate with `@workflow_defn`.
393
+ # We'll rely on them passing the classes or scanning your code.
394
+
395
+ self._worker = Worker(
396
+ client=self.client,
397
+ task_queue=self.config.task_queue,
398
+ activities=activities,
399
+ workflows=[], # We'll auto-load by Python scanning or let the user specify
400
+ )
401
+ print(
402
+ f"Starting Temporal Worker on task queue '{self.config.task_queue}' with {len(activities)} activities."
403
+ )
404
+
405
+ await self._worker.run()
@@ -0,0 +1,197 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ Generic,
7
+ TypeVar,
8
+ Union,
9
+ )
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+ from mcp_agent.executor.executor import Executor
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ class WorkflowState(BaseModel):
19
+ """
20
+ Simple container for persistent workflow state.
21
+ This can hold fields that should persist across tasks.
22
+ """
23
+
24
+ status: str = "initialized"
25
+ metadata: Dict[str, Any] = Field(default_factory=dict)
26
+ updated_at: float | None = None
27
+ error: Dict[str, Any] | None = None
28
+
29
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
30
+
31
+ def record_error(self, error: Exception) -> None:
32
+ self.error = {
33
+ "type": type(error).__name__,
34
+ "message": str(error),
35
+ "timestamp": datetime.utcnow().timestamp(),
36
+ }
37
+
38
+
39
+ class WorkflowResult(BaseModel, Generic[T]):
40
+ value: Union[T, None] = None
41
+ metadata: Dict[str, Any] = Field(default_factory=dict)
42
+ start_time: float | None = None
43
+ end_time: float | None = None
44
+
45
+
46
+ class Workflow(ABC, Generic[T]):
47
+ """
48
+ Base class for user-defined workflows.
49
+ Handles execution and state management.
50
+ Some key notes:
51
+ - To enable the executor engine to recognize and orchestrate the workflow,
52
+ - the class MUST be decorated with @workflow.
53
+ - the main entrypoint method MUST be decorated with @workflow_run.
54
+ - any task methods MUST be decorated with @workflow_task.
55
+
56
+ - Persistent state: Provides a simple `state` object for storing data across tasks.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ executor: Executor,
62
+ name: str | None = None,
63
+ metadata: Dict[str, Any] | None = None,
64
+ **kwargs: Any,
65
+ ):
66
+ self.executor = executor
67
+ self.name = name or self.__class__.__name__
68
+ self.init_kwargs = kwargs
69
+ # TODO: handle logging
70
+ # self._logger = logging.getLogger(self.name)
71
+
72
+ # A simple workflow state object
73
+ # If under Temporal, storing it as a field on this class
74
+ # means it can be replayed automatically
75
+ self.state = WorkflowState(name=name, metadata=metadata or {})
76
+
77
+ @abstractmethod
78
+ async def run(self, *args: Any, **kwargs: Any) -> "WorkflowResult[T]":
79
+ """
80
+ Main workflow implementation. Must be overridden by subclasses.
81
+ """
82
+
83
+ async def update_state(self, **kwargs):
84
+ """Syntactic sugar to update workflow state."""
85
+ for key, value in kwargs.items():
86
+ self.state[key] = value
87
+ setattr(self.state, key, value)
88
+
89
+ self.state.updated_at = datetime.utcnow().timestamp()
90
+
91
+ async def wait_for_input(self, description: str = "Provide input") -> str:
92
+ """
93
+ Convenience method for human input. Uses `human_input` signal
94
+ so we can unify local (console input) and Temporal signals.
95
+ """
96
+ return await self.executor.wait_for_signal(
97
+ "human_input", description=description
98
+ )
99
+
100
+
101
+ # ############################
102
+ # # Example: DocumentWorkflow
103
+ # ############################
104
+
105
+
106
+ # @workflow_defn # <-- This becomes @temporal_workflow.defn if in Temporal mode, else no-op
107
+ # class DocumentWorkflow(Workflow[List[Dict[str, Any]]]):
108
+ # """
109
+ # Example workflow with persistent state.
110
+ # If run locally, `self.state` is ephemeral.
111
+ # If run in Temporal mode, `self.state` is replayed automatically.
112
+ # """
113
+
114
+ # @workflow_task(
115
+ # schedule_to_close_timeout=timedelta(minutes=10),
116
+ # retry_policy={"initial_interval": 1, "max_attempts": 3},
117
+ # )
118
+ # async def process_document(self, doc_id: str) -> Dict[str, Any]:
119
+ # """Activity that simulates document processing."""
120
+ # await asyncio.sleep(1)
121
+ # # Optionally mutate workflow state
122
+ # self.state.metadata.setdefault("processed_docs", []).append(doc_id)
123
+ # return {
124
+ # "doc_id": doc_id,
125
+ # "status": "processed",
126
+ # "timestamp": datetime.utcnow().isoformat(),
127
+ # }
128
+
129
+ # @workflow_run # <-- This becomes @temporal_workflow.run(...) if Temporal is used
130
+ # async def _run_impl(
131
+ # self, documents: List[str], batch_size: int = 2
132
+ # ) -> List[Dict[str, Any]]:
133
+ # """Main workflow logic, which becomes the official 'run' in Temporal mode."""
134
+ # self._logger.info("Workflow starting, state=%s", self.state)
135
+ # self.state.update_status("running")
136
+
137
+ # all_results = []
138
+ # for i in range(0, len(documents), batch_size):
139
+ # batch = documents[i : i + batch_size]
140
+ # tasks = [self.process_document(doc) for doc in batch]
141
+ # results = await self.executor.execute(*tasks)
142
+
143
+ # for res in results:
144
+ # if isinstance(res.value, Exception):
145
+ # self._logger.error(
146
+ # f"Error processing document: {res.metadata.get('error')}"
147
+ # )
148
+ # else:
149
+ # all_results.append(res.value)
150
+
151
+ # self.state.update_status("completed")
152
+ # return all_results
153
+
154
+
155
+ # ########################
156
+ # # 12. Example Local Usage
157
+ # ########################
158
+
159
+
160
+ # async def run_example_local():
161
+ # from . import AsyncIOExecutor, DocumentWorkflow # if in a package
162
+
163
+ # executor = AsyncIOExecutor()
164
+ # wf = DocumentWorkflow(executor)
165
+
166
+ # documents = ["doc1", "doc2", "doc3", "doc4"]
167
+ # result = await wf.run(documents, batch_size=2)
168
+
169
+ # print("Local results:", result.value)
170
+ # print("Local workflow final state:", wf.state)
171
+ # # Notice `wf.state.metadata['processed_docs']` has the processed doc IDs.
172
+
173
+
174
+ # ########################
175
+ # # Example Temporal Usage
176
+ # ########################
177
+
178
+
179
+ # async def run_example_temporal():
180
+ # from . import TemporalExecutor, DocumentWorkflow # if in a package
181
+
182
+ # # 1) Create a TemporalExecutor (client side)
183
+ # executor = TemporalExecutor(task_queue="my_task_queue")
184
+ # await executor.ensure_client()
185
+
186
+ # # 2) Start a worker in the same process (or do so in a separate process)
187
+ # asyncio.create_task(executor.start_worker())
188
+ # await asyncio.sleep(2) # Wait for worker to be up
189
+
190
+ # # 3) Now we can run the workflow by normal means if we like,
191
+ # # or rely on the Worker picking it up. Typically, you'd do:
192
+ # # handle = await executor._client.start_workflow(...)
193
+ # # but let's keep it simple and show conceptually
194
+ # # that 'DocumentWorkflow' is now recognized as a real Temporal workflow
195
+ # print(
196
+ # "Temporal environment is running. Use the Worker logs or CLI to start 'DocumentWorkflow'."
197
+ # )