agnt5 0.2.8a10__cp310-abi3-manylinux_2_34_x86_64.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 agnt5 might be problematic. Click here for more details.

agnt5/exceptions.py ADDED
@@ -0,0 +1,102 @@
1
+ """AGNT5 SDK exceptions and error types."""
2
+
3
+ from typing import Dict, List, Optional
4
+
5
+
6
+ class AGNT5Error(Exception):
7
+ """Base exception for all AGNT5 SDK errors."""
8
+
9
+ pass
10
+
11
+
12
+ class ConfigurationError(AGNT5Error):
13
+ """Raised when SDK configuration is invalid."""
14
+
15
+ pass
16
+
17
+
18
+ class ExecutionError(AGNT5Error):
19
+ """Raised when function or workflow execution fails."""
20
+
21
+ pass
22
+
23
+
24
+ class RetryError(ExecutionError):
25
+ """Raised when a function exceeds maximum retry attempts."""
26
+
27
+ def __init__(self, message: str, attempts: int, last_error: Exception) -> None:
28
+ super().__init__(message)
29
+ self.attempts = attempts
30
+ self.last_error = last_error
31
+
32
+
33
+ class StateError(AGNT5Error):
34
+ """Raised when state operations fail."""
35
+
36
+ pass
37
+
38
+
39
+ class CheckpointError(AGNT5Error):
40
+ """Raised when checkpoint operations fail."""
41
+
42
+ pass
43
+
44
+
45
+ class NotImplementedError(AGNT5Error):
46
+ """Raised when a feature is not yet implemented."""
47
+
48
+ pass
49
+
50
+
51
+ class WaitingForUserInputException(AGNT5Error):
52
+ """Raised when workflow needs to pause for user input.
53
+
54
+ This exception is used internally by ctx.wait_for_user() to signal
55
+ that a workflow execution should pause and wait for user input.
56
+
57
+ The platform catches this exception and:
58
+ 1. Saves the workflow checkpoint state
59
+ 2. Returns awaiting_user_input status to the client
60
+ 3. Presents the question and options to the user
61
+ 4. Resumes execution when user responds
62
+
63
+ Attributes:
64
+ question: The question to ask the user
65
+ input_type: Type of input ("text", "approval", or "choice")
66
+ options: List of options for approval/choice inputs
67
+ checkpoint_state: Current workflow state for resume
68
+ agent_context: Optional agent execution state for agent-level HITL
69
+ Contains: agent_name, iteration, messages, tool_results, pending_tool_call, etc.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ question: str,
75
+ input_type: str,
76
+ options: Optional[List[Dict]],
77
+ checkpoint_state: Dict,
78
+ agent_context: Optional[Dict] = None,
79
+ ) -> None:
80
+ """Initialize WaitingForUserInputException.
81
+
82
+ Args:
83
+ question: Question to ask the user
84
+ input_type: Type of input - "text", "approval", or "choice"
85
+ options: List of option dicts (for approval/choice)
86
+ checkpoint_state: Workflow state snapshot for resume
87
+ agent_context: Optional agent execution state for resuming agents
88
+ Required fields when provided:
89
+ - agent_name: Name of the agent that paused
90
+ - iteration: Current iteration number (0-indexed)
91
+ - messages: LLM conversation history as list of dicts
92
+ - tool_results: Partial tool results for current iteration
93
+ - pending_tool_call: The HITL tool call awaiting response
94
+ - all_tool_calls: All tool calls made so far
95
+ - model_config: Model settings for resume
96
+ """
97
+ super().__init__(f"Waiting for user input: {question}")
98
+ self.question = question
99
+ self.input_type = input_type
100
+ self.options = options or []
101
+ self.checkpoint_state = checkpoint_state
102
+ self.agent_context = agent_context
agnt5/function.py ADDED
@@ -0,0 +1,321 @@
1
+ """Function component implementation for AGNT5 SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import functools
7
+ import inspect
8
+ import uuid
9
+ from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar, Union, cast
10
+
11
+ from ._retry_utils import execute_with_retry, parse_backoff_policy, parse_retry_policy
12
+ from ._schema_utils import extract_function_metadata, extract_function_schemas
13
+ from .context import Context, set_current_context
14
+ from .exceptions import RetryError
15
+ from .types import BackoffPolicy, BackoffType, FunctionConfig, HandlerFunc, RetryPolicy
16
+
17
+ T = TypeVar("T")
18
+
19
+ # Global function registry
20
+ _FUNCTION_REGISTRY: Dict[str, FunctionConfig] = {}
21
+
22
+ class FunctionContext(Context):
23
+ """
24
+ Lightweight context for stateless functions.
25
+
26
+ AGNT5 Philosophy: Context is a convenience, not a requirement.
27
+ The best function is one that doesn't need context at all!
28
+
29
+ Provides only:
30
+ - Quick logging (ctx.log())
31
+ - Execution metadata (run_id, attempt)
32
+ - Smart retry helper (should_retry())
33
+ - Non-durable sleep
34
+
35
+ Does NOT provide:
36
+ - Orchestration (task, parallel, gather) - use workflows
37
+ - State management (get, set, delete) - functions are stateless
38
+ - Checkpointing (step) - functions are atomic
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ run_id: str,
44
+ attempt: int = 0,
45
+ runtime_context: Optional[Any] = None,
46
+ retry_policy: Optional[Any] = None,
47
+ ) -> None:
48
+ """
49
+ Initialize function context.
50
+
51
+ Args:
52
+ run_id: Unique execution identifier
53
+ attempt: Retry attempt number (0-indexed)
54
+ runtime_context: RuntimeContext for trace correlation
55
+ retry_policy: RetryPolicy for should_retry() checks
56
+ """
57
+ super().__init__(run_id, attempt, runtime_context)
58
+ self._retry_policy = retry_policy
59
+
60
+ # === Quick Logging ===
61
+
62
+ def log(self, message: str, **extra) -> None:
63
+ """
64
+ Quick logging shorthand with structured data.
65
+
66
+ Example:
67
+ ctx.log("Processing payment", amount=100.50, user_id="123")
68
+ """
69
+ self._logger.info(message, extra=extra)
70
+
71
+ # === Smart Execution ===
72
+
73
+ def should_retry(self, error: Exception) -> bool:
74
+ """
75
+ Check if error is retryable based on configured policy.
76
+
77
+ Example:
78
+ try:
79
+ result = await external_api()
80
+ except Exception as e:
81
+ if not ctx.should_retry(e):
82
+ raise # Fail fast for non-retryable errors
83
+ # Otherwise let retry policy handle it
84
+ raise
85
+
86
+ Returns:
87
+ True if error is retryable, False otherwise
88
+ """
89
+ # TODO: Implement retry policy checks
90
+ # For now, all errors are retryable (let retry policy handle it)
91
+ return True
92
+
93
+ async def sleep(self, seconds: float) -> None:
94
+ """
95
+ Non-durable async sleep.
96
+
97
+ For durable sleep across failures, use workflows.
98
+
99
+ Args:
100
+ seconds: Number of seconds to sleep
101
+ """
102
+ import asyncio
103
+ await asyncio.sleep(seconds)
104
+
105
+
106
+
107
+ class FunctionRegistry:
108
+ """Registry for function handlers."""
109
+
110
+ @staticmethod
111
+ def register(config: FunctionConfig) -> None:
112
+ """Register a function handler.
113
+
114
+ Args:
115
+ config: Function configuration to register
116
+
117
+ Raises:
118
+ ValueError: If a function with the same name is already registered
119
+ """
120
+ # Check for name collision
121
+ if config.name in _FUNCTION_REGISTRY:
122
+ existing_config = _FUNCTION_REGISTRY[config.name]
123
+ existing_module = existing_config.handler.__module__
124
+ new_module = config.handler.__module__
125
+
126
+ raise ValueError(
127
+ f"Function name collision: '{config.name}' is already registered.\n"
128
+ f" Existing: {existing_module}.{existing_config.handler.__name__}\n"
129
+ f" New: {new_module}.{config.handler.__name__}\n"
130
+ f"Please use a different function name or use name= parameter to specify a unique name."
131
+ )
132
+
133
+ _FUNCTION_REGISTRY[config.name] = config
134
+
135
+ @staticmethod
136
+ def get(name: str) -> Optional[FunctionConfig]:
137
+ """Get function configuration by name."""
138
+ return _FUNCTION_REGISTRY.get(name)
139
+
140
+ @staticmethod
141
+ def all() -> Dict[str, FunctionConfig]:
142
+ """Get all registered functions."""
143
+ return _FUNCTION_REGISTRY.copy()
144
+
145
+ @staticmethod
146
+ def clear() -> None:
147
+ """Clear all registered functions."""
148
+ _FUNCTION_REGISTRY.clear()
149
+
150
+
151
+ def function(
152
+ _func: Optional[Callable[..., Any]] = None,
153
+ *,
154
+ name: Optional[str] = None,
155
+ retries: Optional[Union[int, Dict[str, Any], RetryPolicy]] = None,
156
+ backoff: Optional[Union[str, Dict[str, Any], BackoffPolicy]] = None,
157
+ ) -> Callable[..., Any]:
158
+ """
159
+ Decorator to mark a function as an AGNT5 durable function.
160
+
161
+ Args:
162
+ name: Custom function name (default: function's __name__)
163
+ retries: Retry policy configuration. Can be:
164
+ - int: max attempts (e.g., 5)
165
+ - dict: RetryPolicy params (e.g., {"max_attempts": 5, "initial_interval_ms": 1000})
166
+ - RetryPolicy: full policy object
167
+ backoff: Backoff policy for retries. Can be:
168
+ - str: backoff type ("constant", "linear", "exponential")
169
+ - dict: BackoffPolicy params (e.g., {"type": "exponential", "multiplier": 2.0})
170
+ - BackoffPolicy: full policy object
171
+
172
+ Note:
173
+ Sync Functions: Synchronous functions are automatically executed in a thread pool
174
+ to prevent blocking the event loop. This is ideal for I/O-bound operations
175
+ (requests.get(), file I/O, etc.). For CPU-bound operations or when you need
176
+ explicit control over concurrency, use async functions instead.
177
+
178
+ Example:
179
+ # Basic function with context
180
+ @function
181
+ async def greet(ctx: FunctionContext, name: str) -> str:
182
+ ctx.log(f"Greeting {name}") # AGNT5 shorthand!
183
+ return f"Hello, {name}!"
184
+
185
+ # Simple function without context (optional)
186
+ @function
187
+ async def add(a: int, b: int) -> int:
188
+ return a + b
189
+
190
+ # With Pydantic models (automatic validation + rich schemas)
191
+ from pydantic import BaseModel
192
+
193
+ class UserInput(BaseModel):
194
+ name: str
195
+ age: int
196
+
197
+ class UserOutput(BaseModel):
198
+ greeting: str
199
+ is_adult: bool
200
+
201
+ @function
202
+ async def process_user(ctx: FunctionContext, user: UserInput) -> UserOutput:
203
+ ctx.log(f"Processing user {user.name}")
204
+ return UserOutput(
205
+ greeting=f"Hello, {user.name}!",
206
+ is_adult=user.age >= 18
207
+ )
208
+
209
+ # Simple retry count
210
+ @function(retries=5)
211
+ async def with_retries(data: str) -> str:
212
+ return data.upper()
213
+
214
+ # Dict configuration
215
+ @function(retries={"max_attempts": 5}, backoff="exponential")
216
+ async def advanced(a: int, b: int) -> int:
217
+ return a + b
218
+ """
219
+
220
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
221
+ # Get function name
222
+ func_name = name or func.__name__
223
+
224
+ # Validate function signature and check if context is needed
225
+ sig = inspect.signature(func)
226
+ params = list(sig.parameters.values())
227
+
228
+ # Check if function declares 'ctx' parameter
229
+ needs_context = params and params[0].name == "ctx"
230
+
231
+ # Convert sync to async if needed
232
+ # Note: Async generators should NOT be wrapped - they need to be returned as-is
233
+ if inspect.iscoroutinefunction(func) or inspect.isasyncgenfunction(func):
234
+ handler_func = cast(HandlerFunc, func)
235
+ else:
236
+ # Wrap sync function to run in thread pool (prevents blocking event loop)
237
+ @functools.wraps(func)
238
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
239
+ loop = asyncio.get_running_loop()
240
+ # Run sync function in thread pool executor to prevent blocking
241
+ return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
242
+
243
+ handler_func = cast(HandlerFunc, async_wrapper)
244
+
245
+ # Extract schemas from type hints
246
+ input_schema, output_schema = extract_function_schemas(func)
247
+
248
+ # Extract metadata (description, etc.)
249
+ metadata = extract_function_metadata(func)
250
+
251
+ # Parse retry and backoff policies from flexible formats
252
+ retry_policy = parse_retry_policy(retries)
253
+ backoff_policy = parse_backoff_policy(backoff)
254
+
255
+ # Register function
256
+ config = FunctionConfig(
257
+ name=func_name,
258
+ handler=handler_func,
259
+ retries=retry_policy,
260
+ backoff=backoff_policy,
261
+ input_schema=input_schema,
262
+ output_schema=output_schema,
263
+ metadata=metadata,
264
+ )
265
+ FunctionRegistry.register(config)
266
+
267
+ # Create wrapper with retry logic
268
+ @functools.wraps(func)
269
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
270
+ # Extract or create context based on function signature
271
+ if needs_context:
272
+ # Function declares ctx parameter - first argument must be FunctionContext
273
+ if not args or not isinstance(args[0], FunctionContext):
274
+ raise TypeError(
275
+ f"Function '{func_name}' requires FunctionContext as first argument. "
276
+ f"Usage: await {func_name}(ctx, ...)"
277
+ )
278
+ ctx = args[0]
279
+ func_args = args[1:]
280
+ else:
281
+ # Function doesn't use context - create a minimal one for internal use
282
+ # But first check if a context was passed anyway (for Worker execution)
283
+ if args and isinstance(args[0], FunctionContext):
284
+ # Context was provided by Worker - use it but don't pass to function
285
+ ctx = args[0]
286
+ func_args = args[1:]
287
+ else:
288
+ # No context provided - create a default one
289
+ ctx = FunctionContext(
290
+ run_id=f"local-{uuid.uuid4().hex[:8]}",
291
+ retry_policy=retry_policy
292
+ )
293
+ func_args = args
294
+
295
+ # Set context in task-local storage for automatic propagation
296
+ token = set_current_context(ctx)
297
+ try:
298
+ # Execute with retry
299
+ return await execute_with_retry(
300
+ handler_func,
301
+ ctx,
302
+ config.retries or RetryPolicy(),
303
+ config.backoff or BackoffPolicy(),
304
+ needs_context,
305
+ *func_args,
306
+ **kwargs,
307
+ )
308
+ finally:
309
+ # Always reset context to prevent leakage
310
+ from .context import _current_context
311
+ _current_context.reset(token)
312
+
313
+ # Store config on wrapper for introspection
314
+ wrapper._agnt5_config = config # type: ignore
315
+ return wrapper
316
+
317
+ # Handle both @function and @function(...) syntax
318
+ if _func is None:
319
+ return decorator
320
+ else:
321
+ return decorator(_func)