Skip to content

Commit 3316e62

Browse files
committed
Release version 0.6.0 with significant enhancements and new features
- Introduced a TaskRoutes extension mechanism, allowing custom TaskRoutes injection via `task_routes_class` parameter in `create_a2a_server()` and `_create_request_handler()`, eliminating the need for monkey patching. - Added Task Tree lifecycle hooks with a new `register_task_tree_hook()` decorator for tracking events such as tree creation, start, completion, and failure. - Implemented executor-specific hooks for pre and post-execution logic, enhancing task execution flexibility. - Comprehensive documentation updates for new hook types and usage examples. - Removed the redundant `decorators.py` file, consolidating functionality into `core/decorators.py`. - Updated version references across project files to reflect the new release. - Added tests to validate the new features and ensure robust functionality.
1 parent b6accc3 commit 3316e62

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2761
-126
lines changed

CHANGELOG.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,77 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## [0.6.0] 2025-12-10
9+
10+
### Added
11+
- **TaskRoutes Extension Mechanism**
12+
- Added `task_routes_class` parameter to `create_a2a_server()` and `_create_request_handler()` for custom TaskRoutes injection
13+
- Eliminates the need for monkey patching when extending TaskRoutes functionality
14+
- Supports custom routes via `custom_routes` parameter in `CustomA2AStarletteApplication`
15+
- Backward compatible: optional parameter with default `TaskRoutes` class
16+
- Usage: `create_a2a_server(task_routes_class=CustomTaskRoutes, custom_routes=[...])`
17+
18+
- **Task Tree Lifecycle Hooks**
19+
- New `register_task_tree_hook()` decorator for task tree lifecycle events
20+
- Four hook types: `on_tree_created`, `on_tree_started`, `on_tree_completed`, `on_tree_failed`
21+
- Explicit lifecycle tracking without manual root task detection
22+
- Hooks receive root task and relevant context (status, error message)
23+
- Usage: `@register_task_tree_hook("on_tree_completed") async def on_completed(root_task, status): ...`
24+
25+
- **Executor-Specific Hooks**
26+
- Added `pre_hook` and `post_hook` parameters to `@executor_register()` decorator
27+
- Runtime hook registration via `add_executor_hook(executor_id, hook_type, hook_func)`
28+
- Inject custom logic (e.g., quota checks, demo data fallback) for specific executors
29+
- `pre_hook` can return a result to skip executor execution (useful for demo mode)
30+
- `post_hook` receives executor, task, inputs, and result for post-processing
31+
- Supports both decorator-based and runtime registration for existing executors
32+
33+
- **Automatic user_id Extraction**
34+
- Automatic `user_id` extraction from JWT token in `TaskRoutes.handle_task_generate` and `handle_task_create`
35+
- Only extracts from JWT token payload for security (HTTP headers can be spoofed)
36+
- Supports `user_id` field or standard JWT `sub` claim in token payload
37+
- Extracted `user_id` automatically set on task data
38+
- Simplifies custom route implementations and ensures consistent user identification
39+
- Security: Only trusted JWT tokens are used, no fallback to HTTP headers
40+
41+
- **Demo Mode Support**
42+
- Built-in demo mode via `use_demo` parameter in task inputs
43+
- CLI support: `--use-demo` flag for `apflow run flow` command
44+
- API support: `use_demo` parameter in task creation and execution
45+
- Executors can override `get_demo_result()` method in `BaseTask` for custom demo data
46+
- Default demo data format: `{"result": "Demo execution result", "demo_mode": True}`
47+
- All built-in executors now implement `get_demo_result()` method:
48+
- `SystemInfoExecutor`, `CommandExecutor`, `AggregateResultsExecutor`
49+
- `RestExecutor`, `GenerateExecutor`, `ApiExecutor`
50+
- `SshExecutor`, `GrpcExecutor`, `WebSocketExecutor`
51+
- `McpExecutor`, `DockerExecutor`
52+
- `CrewManager`, `BatchManager` (CrewAI executors)
53+
- **Realistic Demo Execution Timing**: All executors include `_demo_sleep` values to simulate real execution time:
54+
- Network operations (HTTP, SSH, API): 0.2-0.5 seconds
55+
- Container operations (Docker): 1.0 second
56+
- LLM operations (CrewAI, Generate): 1.0-1.5 seconds
57+
- Local operations (SystemInfo, Command, Aggregate): 0.05-0.1 seconds
58+
- **Global Demo Sleep Scale**: Configurable via `AIPARTNERUPFLOW_DEMO_SLEEP_SCALE` environment variable (default: 1.0)
59+
- Allows adjusting demo execution speed globally (e.g., `0.5` for faster, `2.0` for slower)
60+
- API: `set_demo_sleep_scale(scale)` and `get_demo_sleep_scale()` functions
61+
- **CrewAI Demo Support**: `CrewManager` and `BatchManager` generate realistic demo results:
62+
- Based on `works` definition (agents and tasks) from task params or schemas
63+
- Includes simulated `token_usage` matching real LLM execution patterns
64+
- `BatchManager` aggregates token usage across multiple works
65+
- Demo mode helps developers test workflows without external dependencies
66+
67+
- **TaskModel Customization Improvements**
68+
- Enhanced `set_task_model_class()` with improved validation and error messages
69+
- New `@task_model_register()` decorator for convenient TaskModel registration
70+
- Validation ensures custom classes inherit from `TaskModel` with helpful error messages
71+
- Supports `__table_args__ = {'extend_existing': True}` for extending existing table definitions
72+
- Better support for user-defined `MyTaskModel(TaskModel)` with additional fields
73+
74+
- **Documentation for Hook Types**
75+
- Added comprehensive documentation explaining differences between hook types
76+
- `pre_hook` / `post_hook`: Task-level hooks for individual task execution
77+
- `task_tree_hook`: Task tree-level hooks for tree lifecycle events
78+
- Clear usage scenarios and examples in `docs/development/extending.md`
979

1080
### Changed
1181
- **LLM Model Parameter Naming**: Unified LLM model parameter naming to `model` across all components
@@ -19,6 +89,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1989
- **Impact**: Only affects generate functionality introduced in 0.5.0, minimal breaking change
2090
- **Migration**: Update any code using `llm_model` parameter to use `model` instead
2191

92+
### Removed
93+
- **Redundant decorators.py file**
94+
- Removed `src/aipartnerupflow/decorators.py` as it was no longer used
95+
- Functionality superseded by `src/aipartnerupflow/core/decorators.py`
96+
- No impact on existing code (file was not imported by any other modules)
97+
2298

2399
## [0.5.0] 2025-12-7
24100

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "aipartnerupflow"
7-
version = "0.5.0"
7+
version = "0.6.0"
88
description = "Agent workflow orchestration and execution platform"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/aipartnerupflow/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
Protocol Standard: A2A (Agent-to-Agent) Protocol
2020
"""
2121

22-
__version__ = "0.5.0"
22+
__version__ = "0.6.0"
2323

2424
# Core framework - re-export from core module for convenience
2525
from aipartnerupflow.core import (
@@ -38,8 +38,11 @@
3838
from aipartnerupflow.core.decorators import (
3939
register_pre_hook,
4040
register_post_hook,
41+
register_task_tree_hook,
42+
get_task_tree_hooks,
4143
set_task_model_class,
4244
get_task_model_class,
45+
task_model_register,
4346
clear_config,
4447
set_use_task_creator,
4548
get_use_task_creator,
@@ -51,6 +54,9 @@
5154
tool_register,
5255
)
5356

57+
# Extension registry utilities
58+
from aipartnerupflow.core.extensions import add_executor_hook
59+
5460
__all__ = [
5561
# Core framework (from core module)
5662
"ExecutableTask",
@@ -65,8 +71,11 @@
6571
# Unified decorators (Flask-style API)
6672
"register_pre_hook",
6773
"register_post_hook",
74+
"register_task_tree_hook",
75+
"get_task_tree_hooks",
6876
"set_task_model_class",
6977
"get_task_model_class",
78+
"task_model_register",
7079
"clear_config",
7180
"set_use_task_creator",
7281
"get_use_task_creator",
@@ -76,6 +85,8 @@
7685
"storage_register",
7786
"hook_register",
7887
"tool_register",
88+
# Extension registry utilities
89+
"add_executor_hook",
7990
# Version
8091
"__version__",
8192
]

src/aipartnerupflow/api/a2a/agent_executor.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,28 @@ async def _execute_simple_mode(
235235
if require_existing_tasks is None:
236236
require_existing_tasks = None # Use global configuration
237237

238+
# Extract use_demo from metadata, message.metadata, or configuration
239+
use_demo = False
240+
if context.metadata:
241+
use_demo = context.metadata.get("use_demo", False)
242+
# Also check message.metadata if available (A2A protocol supports metadata in message)
243+
if not use_demo and context.message and hasattr(context.message, "metadata"):
244+
message_metadata = context.message.metadata
245+
if isinstance(message_metadata, dict):
246+
use_demo = message_metadata.get("use_demo", False)
247+
if not use_demo and context.configuration:
248+
try:
249+
config_dict = context.configuration.model_dump(exclude_none=True) if hasattr(context.configuration, 'model_dump') else context.configuration.dict(exclude_none=True)
250+
use_demo = config_dict.get("use_demo", False)
251+
except (AttributeError, TypeError):
252+
pass
253+
238254
execution_result = await self.task_executor.execute_tasks(
239255
tasks=tasks,
240256
root_task_id=None, # Use actual root task ID from created task tree
241257
use_streaming=False,
242258
require_existing_tasks=require_existing_tasks, # Allow override via metadata
259+
use_demo=use_demo,
243260
db_session=db_session
244261
)
245262

@@ -419,12 +436,29 @@ async def _execute_streaming_mode(
419436
# Behavior controlled by global configuration (get_require_existing_tasks())
420437
# Default: require_existing_tasks=False (auto-create for convenience)
421438
# root_task_id=None means use the actual root task ID from the created task tree
439+
# Extract use_demo from metadata, message.metadata, or configuration
440+
use_demo = False
441+
if context.metadata:
442+
use_demo = context.metadata.get("use_demo", False)
443+
# Also check message.metadata if available (A2A protocol supports metadata in message)
444+
if not use_demo and context.message and hasattr(context.message, "metadata"):
445+
message_metadata = context.message.metadata
446+
if isinstance(message_metadata, dict):
447+
use_demo = message_metadata.get("use_demo", False)
448+
if not use_demo and context.configuration:
449+
try:
450+
config_dict = context.configuration.model_dump(exclude_none=True) if hasattr(context.configuration, 'model_dump') else context.configuration.dict(exclude_none=True)
451+
use_demo = config_dict.get("use_demo", False)
452+
except (AttributeError, TypeError):
453+
pass
454+
422455
execution_result = await self.task_executor.execute_tasks(
423456
tasks=tasks,
424457
root_task_id=None, # Use actual root task ID from created task tree
425458
use_streaming=True,
426459
streaming_callbacks_context=event_queue_bridge,
427460
require_existing_tasks=None, # Use global configuration (default: False, auto-create)
461+
use_demo=use_demo,
428462
db_session=db_session
429463
)
430464

src/aipartnerupflow/api/a2a/custom_starlette_app.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ def __init__(
164164
enable_system_routes: bool = True,
165165
enable_docs: bool = True,
166166
task_model_class: Optional[Type[TaskModel]] = None,
167+
task_routes_class: Optional[Type[TaskRoutes]] = None,
168+
custom_routes: Optional[List[Route]] = None,
167169
**kwargs
168170
):
169171
"""
@@ -194,6 +196,12 @@ def __init__(
194196
Users can pass their custom TaskModel subclass that inherits TaskModel
195197
to add custom fields (e.g., project_id, department, etc.).
196198
If None, default TaskModel will be used.
199+
task_routes_class: Optional custom TaskRoutes class to use instead of default TaskRoutes.
200+
Allows extending TaskRoutes functionality without monkey patching.
201+
If None, default TaskRoutes will be used.
202+
custom_routes: Optional list of custom Starlette Route objects to add to the application.
203+
Routes are merged after default routes (custom routes can override defaults if needed).
204+
If None, no custom routes are added.
197205
**kwargs: Keyword arguments for A2AStarletteApplication
198206
"""
199207
super().__init__(*args, **kwargs)
@@ -211,8 +219,14 @@ def __init__(
211219
# Store task_model_class for task management APIs
212220
self.task_model_class = task_model_class or TaskModel
213221

222+
# Store custom routes
223+
self.custom_routes = custom_routes or []
224+
225+
# Use provided task_routes_class or default TaskRoutes
226+
task_routes_cls = task_routes_class or TaskRoutes
227+
214228
# Initialize protocol-agnostic route handlers
215-
self.task_routes = TaskRoutes(
229+
self.task_routes = task_routes_cls(
216230
task_model_class=self.task_model_class,
217231
verify_token_func=self.verify_token_func,
218232
verify_permission_func=self.verify_permission_func
@@ -353,6 +367,10 @@ def routes(
353367
),
354368
])
355369

370+
# Add user-provided custom routes
371+
if self.custom_routes:
372+
custom_routes.extend(self.custom_routes)
373+
356374
# Combine standard routes with custom routes
357375
return app_routes + custom_routes
358376

src/aipartnerupflow/api/a2a/server.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
import httpx
6-
from typing import Optional
6+
from typing import Optional, Type
77
from a2a.server.request_handlers import DefaultRequestHandler
88
from a2a.server.tasks import (
99
InMemoryTaskStore,
@@ -207,7 +207,11 @@ def load_skills() -> list[AgentSkill]:
207207
config_store=push_config_store
208208
)
209209

210-
def _create_request_handler(verify_token_func=None, verify_permission_func=None):
210+
def _create_request_handler(
211+
verify_token_func=None,
212+
verify_permission_func=None,
213+
task_routes_class: Optional[Type[TaskRoutes]] = None,
214+
):
211215
"""
212216
Create request handler with agent executor
213217
@@ -217,12 +221,16 @@ def _create_request_handler(verify_token_func=None, verify_permission_func=None)
217221
Args:
218222
verify_token_func: Optional JWT verification function
219223
verify_permission_func: Optional permission verification function
224+
task_routes_class: Optional custom TaskRoutes class (default: TaskRoutes)
220225
"""
221226
# Get task_model_class from registry
222227
task_model_class = get_task_model_class()
223228

229+
# Use provided task_routes_class or default TaskRoutes
230+
task_routes_cls = task_routes_class or TaskRoutes
231+
224232
# Create TaskRoutes instance for the adapter
225-
task_routes = TaskRoutes(
233+
task_routes = task_routes_cls(
226234
task_model_class=task_model_class,
227235
verify_token_func=verify_token_func,
228236
verify_permission_func=verify_permission_func
@@ -276,6 +284,7 @@ def create_a2a_server(
276284
base_url: Optional[str] = None,
277285
enable_system_routes: bool = True,
278286
enable_docs: bool = True,
287+
task_routes_class: Optional[Type[TaskRoutes]] = None,
279288
) -> CustomA2AStarletteApplication:
280289
"""
281290
Create A2A server instance with configuration
@@ -311,6 +320,9 @@ async def my_post_hook(task, inputs, result):
311320
enable_system_routes: Whether to enable system routes like /system (default: True)
312321
enable_docs: Whether to enable interactive API documentation at /docs (default: True).
313322
Only available when API server is running, not when used as a library.
323+
task_routes_class: Optional custom TaskRoutes class to use instead of default TaskRoutes.
324+
Allows extending TaskRoutes functionality without monkey patching.
325+
Example: task_routes_class=MyCustomTaskRoutes
314326
315327
Returns:
316328
CustomA2AStarletteApplication instance
@@ -328,7 +340,8 @@ async def my_post_hook(task, inputs, result):
328340
# Permission checking will be handled at the middleware level
329341
request_handler = _create_request_handler(
330342
verify_token_func=verify_token_func,
331-
verify_permission_func=None
343+
verify_permission_func=None,
344+
task_routes_class=task_routes_class,
332345
)
333346

334347
# Create agent card
@@ -361,5 +374,6 @@ def verify_token_func_callback(token: str) -> Optional[dict]:
361374
enable_system_routes=enable_system_routes,
362375
enable_docs=enable_docs,
363376
task_model_class=final_task_model_class,
377+
task_routes_class=task_routes_class,
364378
)
365379

src/aipartnerupflow/api/routes/base.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,39 @@ def _get_user_info(self, request: Request) -> Tuple[Optional[str], Optional[list
6565
roles = [roles]
6666
return user_id, roles
6767

68+
def _extract_user_id_from_request(self, request: Request) -> Optional[str]:
69+
"""
70+
Extract user_id from request JWT token
71+
72+
This method extracts user_id from JWT token payload.
73+
Only JWT tokens are trusted for security reasons - HTTP headers
74+
can be easily spoofed by clients and should not be used for
75+
authentication or user identification.
76+
77+
Args:
78+
request: Starlette request object
79+
80+
Returns:
81+
User ID string from JWT token payload, or None if not found
82+
"""
83+
# Only extract from JWT token (if verify_token_func is available)
84+
if self.verify_token_func:
85+
auth_header = request.headers.get("Authorization")
86+
if auth_header and auth_header.startswith("Bearer "):
87+
token = auth_header.split(" ", 1)[1]
88+
try:
89+
payload = self.verify_token_func(token)
90+
if payload and "user_id" in payload:
91+
return payload["user_id"]
92+
# Also try "sub" field (standard JWT claim)
93+
if payload and "sub" in payload:
94+
return payload["sub"]
95+
except Exception:
96+
# Token verification failed
97+
pass
98+
99+
return None
100+
68101
def _check_permission(
69102
self,
70103
request: Request,

0 commit comments

Comments
 (0)