agentscope-runtime 0.1.5b2__py3-none-any.whl → 0.2.0b1__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 (105) hide show
  1. agentscope_runtime/common/__init__.py +0 -0
  2. agentscope_runtime/common/collections/in_memory_mapping.py +27 -0
  3. agentscope_runtime/common/collections/redis_mapping.py +42 -0
  4. agentscope_runtime/common/container_clients/__init__.py +0 -0
  5. agentscope_runtime/common/container_clients/agentrun_client.py +1098 -0
  6. agentscope_runtime/common/container_clients/docker_client.py +250 -0
  7. agentscope_runtime/engine/__init__.py +12 -0
  8. agentscope_runtime/engine/agents/agentscope_agent.py +488 -0
  9. agentscope_runtime/engine/agents/agno_agent.py +19 -18
  10. agentscope_runtime/engine/agents/autogen_agent.py +13 -8
  11. agentscope_runtime/engine/agents/utils.py +53 -0
  12. agentscope_runtime/engine/app/__init__.py +6 -0
  13. agentscope_runtime/engine/app/agent_app.py +239 -0
  14. agentscope_runtime/engine/app/base_app.py +181 -0
  15. agentscope_runtime/engine/app/celery_mixin.py +92 -0
  16. agentscope_runtime/engine/deployers/base.py +1 -0
  17. agentscope_runtime/engine/deployers/cli_fc_deploy.py +39 -20
  18. agentscope_runtime/engine/deployers/kubernetes_deployer.py +12 -5
  19. agentscope_runtime/engine/deployers/local_deployer.py +61 -3
  20. agentscope_runtime/engine/deployers/modelstudio_deployer.py +10 -11
  21. agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +9 -0
  22. agentscope_runtime/engine/deployers/utils/package_project_utils.py +234 -3
  23. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +567 -7
  24. agentscope_runtime/engine/deployers/utils/service_utils/standalone_main.py.j2 +211 -0
  25. agentscope_runtime/engine/deployers/utils/wheel_packager.py +1 -1
  26. agentscope_runtime/engine/helpers/helper.py +60 -41
  27. agentscope_runtime/engine/runner.py +35 -24
  28. agentscope_runtime/engine/schemas/agent_schemas.py +42 -0
  29. agentscope_runtime/engine/schemas/modelstudio_llm.py +14 -14
  30. agentscope_runtime/engine/services/sandbox_service.py +62 -70
  31. agentscope_runtime/engine/services/tablestore_memory_service.py +304 -0
  32. agentscope_runtime/engine/services/tablestore_rag_service.py +143 -0
  33. agentscope_runtime/engine/services/tablestore_session_history_service.py +293 -0
  34. agentscope_runtime/engine/services/utils/__init__.py +0 -0
  35. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +352 -0
  36. agentscope_runtime/engine/tracing/__init__.py +9 -3
  37. agentscope_runtime/engine/tracing/asyncio_util.py +24 -0
  38. agentscope_runtime/engine/tracing/base.py +66 -34
  39. agentscope_runtime/engine/tracing/local_logging_handler.py +45 -31
  40. agentscope_runtime/engine/tracing/message_util.py +528 -0
  41. agentscope_runtime/engine/tracing/tracing_metric.py +20 -8
  42. agentscope_runtime/engine/tracing/tracing_util.py +130 -0
  43. agentscope_runtime/engine/tracing/wrapper.py +794 -169
  44. agentscope_runtime/sandbox/__init__.py +2 -0
  45. agentscope_runtime/sandbox/box/base/__init__.py +4 -0
  46. agentscope_runtime/sandbox/box/base/base_sandbox.py +6 -4
  47. agentscope_runtime/sandbox/box/browser/__init__.py +4 -0
  48. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +10 -14
  49. agentscope_runtime/sandbox/box/dummy/__init__.py +4 -0
  50. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +2 -1
  51. agentscope_runtime/sandbox/box/filesystem/__init__.py +4 -0
  52. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +10 -7
  53. agentscope_runtime/sandbox/box/gui/__init__.py +4 -0
  54. agentscope_runtime/sandbox/box/gui/box/__init__.py +0 -0
  55. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +81 -0
  56. agentscope_runtime/sandbox/box/sandbox.py +5 -2
  57. agentscope_runtime/sandbox/box/shared/routers/generic.py +20 -1
  58. agentscope_runtime/sandbox/box/training_box/__init__.py +4 -0
  59. agentscope_runtime/sandbox/box/training_box/training_box.py +10 -15
  60. agentscope_runtime/sandbox/build.py +143 -58
  61. agentscope_runtime/sandbox/client/http_client.py +87 -59
  62. agentscope_runtime/sandbox/client/training_client.py +0 -1
  63. agentscope_runtime/sandbox/constant.py +27 -1
  64. agentscope_runtime/sandbox/custom/custom_sandbox.py +7 -6
  65. agentscope_runtime/sandbox/custom/example.py +4 -3
  66. agentscope_runtime/sandbox/enums.py +1 -0
  67. agentscope_runtime/sandbox/manager/sandbox_manager.py +212 -106
  68. agentscope_runtime/sandbox/manager/server/app.py +82 -14
  69. agentscope_runtime/sandbox/manager/server/config.py +50 -3
  70. agentscope_runtime/sandbox/model/container.py +12 -23
  71. agentscope_runtime/sandbox/model/manager_config.py +93 -5
  72. agentscope_runtime/sandbox/registry.py +1 -1
  73. agentscope_runtime/sandbox/tools/gui/__init__.py +7 -0
  74. agentscope_runtime/sandbox/tools/gui/tool.py +77 -0
  75. agentscope_runtime/sandbox/tools/mcp_tool.py +6 -2
  76. agentscope_runtime/sandbox/tools/tool.py +4 -0
  77. agentscope_runtime/sandbox/utils.py +124 -0
  78. agentscope_runtime/version.py +1 -1
  79. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0b1.dist-info}/METADATA +209 -101
  80. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0b1.dist-info}/RECORD +94 -78
  81. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +0 -6
  82. agentscope_runtime/engine/agents/agentscope_agent/agent.py +0 -401
  83. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +0 -169
  84. agentscope_runtime/engine/agents/llm_agent.py +0 -51
  85. agentscope_runtime/engine/llms/__init__.py +0 -3
  86. agentscope_runtime/engine/llms/base_llm.py +0 -60
  87. agentscope_runtime/engine/llms/qwen_llm.py +0 -47
  88. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +0 -22
  89. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +0 -26
  90. agentscope_runtime/sandbox/manager/container_clients/__init__.py +0 -10
  91. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +0 -422
  92. /agentscope_runtime/{sandbox/manager → common}/collections/__init__.py +0 -0
  93. /agentscope_runtime/{sandbox/manager → common}/collections/base_mapping.py +0 -0
  94. /agentscope_runtime/{sandbox/manager → common}/collections/base_queue.py +0 -0
  95. /agentscope_runtime/{sandbox/manager → common}/collections/base_set.py +0 -0
  96. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_queue.py +0 -0
  97. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_set.py +0 -0
  98. /agentscope_runtime/{sandbox/manager → common}/collections/redis_queue.py +0 -0
  99. /agentscope_runtime/{sandbox/manager → common}/collections/redis_set.py +0 -0
  100. /agentscope_runtime/{sandbox/manager → common}/container_clients/base_client.py +0 -0
  101. /agentscope_runtime/{sandbox/manager → common}/container_clients/kubernetes_client.py +0 -0
  102. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0b1.dist-info}/WHEEL +0 -0
  103. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0b1.dist-info}/entry_points.txt +0 -0
  104. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0b1.dist-info}/licenses/LICENSE +0 -0
  105. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0b1.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,17 @@
1
1
  # -*- coding: utf-8 -*-
2
- # pylint:disable=too-many-branches, unused-argument
2
+ # pylint:disable=too-many-branches, unused-argument, too-many-return-statements
3
3
 
4
4
 
5
5
  import asyncio
6
+ import inspect
6
7
  import json
7
8
  from contextlib import asynccontextmanager
8
- from typing import Optional, Callable, Type, Any
9
+ from typing import Optional, Callable, Type, Any, List, Dict
9
10
 
10
11
  from fastapi import FastAPI, Request
11
12
  from fastapi.middleware.cors import CORSMiddleware
12
13
  from fastapi.responses import StreamingResponse, JSONResponse
14
+ from pydantic import BaseModel
13
15
 
14
16
  from .service_config import ServicesConfig, DEFAULT_SERVICES_CONFIG
15
17
  from .service_factory import ServiceFactory
@@ -17,6 +19,13 @@ from ..deployment_modes import DeploymentMode
17
19
  from ...adapter.protocol_adapter import ProtocolAdapter
18
20
 
19
21
 
22
+ async def error_stream(e):
23
+ yield (
24
+ f"data: "
25
+ f"{json.dumps({'error': f'Request parsing error: {str(e)}'})}\n\n"
26
+ )
27
+
28
+
20
29
  class FastAPIAppFactory:
21
30
  """Factory for creating FastAPI applications with unified architecture."""
22
31
 
@@ -33,6 +42,13 @@ class FastAPIAppFactory:
33
42
  mode: DeploymentMode = DeploymentMode.DAEMON_THREAD,
34
43
  services_config: Optional[ServicesConfig] = None,
35
44
  protocol_adapters: Optional[list[ProtocolAdapter]] = None,
45
+ custom_endpoints: Optional[
46
+ List[Dict]
47
+ ] = None, # New parameter for custom endpoints
48
+ # Celery parameters
49
+ broker_url: Optional[str] = None,
50
+ backend_url: Optional[str] = None,
51
+ enable_embedded_worker: bool = False,
36
52
  **kwargs: Any,
37
53
  ) -> FastAPI:
38
54
  """Create a FastAPI application with unified architecture.
@@ -49,6 +65,10 @@ class FastAPIAppFactory:
49
65
  mode: Deployment mode
50
66
  services_config: Services configuration
51
67
  protocol_adapters: Protocol adapters
68
+ custom_endpoints: List of custom endpoint configurations
69
+ broker_url: Celery broker URL
70
+ backend_url: Celery backend URL
71
+ enable_embedded_worker: Whether to run embedded Celery worker
52
72
  **kwargs: Additional keyword arguments
53
73
 
54
74
  Returns:
@@ -58,6 +78,20 @@ class FastAPIAppFactory:
58
78
  if services_config is None:
59
79
  services_config = DEFAULT_SERVICES_CONFIG
60
80
 
81
+ # Initialize Celery mixin if broker and backend URLs are provided
82
+ celery_mixin = None
83
+ if broker_url and backend_url:
84
+ try:
85
+ from ....app.celery_mixin import CeleryMixin
86
+
87
+ celery_mixin = CeleryMixin(
88
+ broker_url=broker_url,
89
+ backend_url=backend_url,
90
+ )
91
+ except ImportError:
92
+ # CeleryMixin not available, will use fallback task processing
93
+ celery_mixin = None
94
+
61
95
  # Create lifespan manager
62
96
  @asynccontextmanager
63
97
  async def lifespan(app: FastAPI):
@@ -93,6 +127,15 @@ class FastAPIAppFactory:
93
127
  app.state.external_runner = runner
94
128
  app.state.endpoint_path = endpoint_path
95
129
  app.state.protocol_adapters = protocol_adapters # Store for later use
130
+ app.state.custom_endpoints = (
131
+ custom_endpoints or []
132
+ ) # Store custom endpoints
133
+
134
+ # Store Celery configuration
135
+ app.state.celery_mixin = celery_mixin
136
+ app.state.broker_url = broker_url
137
+ app.state.backend_url = backend_url
138
+ app.state.enable_embedded_worker = enable_embedded_worker
96
139
 
97
140
  # Add middleware
98
141
  FastAPIAppFactory._add_middleware(app, mode)
@@ -168,6 +211,49 @@ class FastAPIAppFactory:
168
211
  for protocol_adapter in app.state.protocol_adapters:
169
212
  protocol_adapter.add_endpoint(app=app, func=effective_func)
170
213
 
214
+ # Add custom endpoints after runner is available
215
+ if (
216
+ hasattr(app.state, "custom_endpoints")
217
+ and app.state.custom_endpoints
218
+ ):
219
+ FastAPIAppFactory._add_custom_endpoints(app)
220
+
221
+ # Start embedded Celery worker if enabled
222
+ if (
223
+ hasattr(app.state, "enable_embedded_worker")
224
+ and app.state.enable_embedded_worker
225
+ and hasattr(app.state, "celery_mixin")
226
+ and app.state.celery_mixin
227
+ ):
228
+ # Start Celery worker in background thread
229
+ import threading
230
+
231
+ def start_celery_worker():
232
+ try:
233
+ celery_mixin = app.state.celery_mixin
234
+ # Get registered queues or use default
235
+ queues = (
236
+ list(celery_mixin.get_registered_queues())
237
+ if celery_mixin.get_registered_queues()
238
+ else ["celery"]
239
+ )
240
+ celery_mixin.run_task_processor(
241
+ loglevel="INFO",
242
+ concurrency=1,
243
+ queues=queues,
244
+ )
245
+ except Exception as e:
246
+ import logging
247
+
248
+ logger = logging.getLogger(__name__)
249
+ logger.error(f"Failed to start Celery worker: {e}")
250
+
251
+ worker_thread = threading.Thread(
252
+ target=start_celery_worker,
253
+ daemon=True,
254
+ )
255
+ worker_thread.start()
256
+
171
257
  @staticmethod
172
258
  async def _handle_shutdown(
173
259
  app: FastAPI,
@@ -188,9 +274,10 @@ class FastAPIAppFactory:
188
274
  and not app.state.runner_managed_externally
189
275
  ):
190
276
  runner = app.state.runner
191
- if runner and hasattr(runner, "context_manager"):
277
+ if runner:
192
278
  try:
193
- await runner.context_manager.__aexit__(None, None, None)
279
+ # Clean up runner
280
+ await runner.__aexit__(None, None, None)
194
281
  except Exception as e:
195
282
  print(f"Warning: Error during runner cleanup: {e}")
196
283
 
@@ -211,15 +298,15 @@ class FastAPIAppFactory:
211
298
  memory_service=services["memory"],
212
299
  )
213
300
 
214
- # Initialize context manager
215
- await context_manager.__aenter__()
216
-
217
301
  # Create runner (agent will be set later)
218
302
  runner = Runner(
219
303
  agent=None, # Will be set by the specific deployment
220
304
  context_manager=context_manager,
221
305
  )
222
306
 
307
+ # Initialize runner
308
+ await runner.__aenter__()
309
+
223
310
  return runner
224
311
 
225
312
  @staticmethod
@@ -502,3 +589,476 @@ class FastAPIAppFactory:
502
589
  if hasattr(app.state, "runner"):
503
590
  return app.state.runner
504
591
  return None
592
+
593
+ @staticmethod
594
+ def _create_parameter_wrapper(handler: Callable):
595
+ """Create a wrapper that handles parameter parsing based on function
596
+ signature.
597
+
598
+ This method inspects the handler function's parameters and creates
599
+ appropriate wrappers to parse request data into the expected
600
+ parameter types.
601
+ """
602
+ try:
603
+ sig = inspect.signature(handler)
604
+ params = list(sig.parameters.values())
605
+
606
+ if not params:
607
+ # No parameters, call function directly
608
+ return handler
609
+
610
+ # Get the first parameter (assuming single parameter for now)
611
+ first_param = params[0]
612
+ param_annotation = first_param.annotation
613
+
614
+ # If no annotation or annotation is Request, pass Request directly
615
+ if param_annotation in [inspect.Parameter.empty, Request]:
616
+ return handler
617
+
618
+ # Check if the annotation is a Pydantic model
619
+ if isinstance(param_annotation, type) and issubclass(
620
+ param_annotation,
621
+ BaseModel,
622
+ ):
623
+ # Create wrapper that parses JSON to Pydantic model
624
+ if inspect.iscoroutinefunction(handler):
625
+
626
+ async def async_pydantic_wrapper(request: Request):
627
+ try:
628
+ body = await request.json()
629
+ parsed_param = param_annotation(**body)
630
+ return await handler(parsed_param)
631
+ except Exception as e:
632
+ return JSONResponse(
633
+ status_code=422,
634
+ content={
635
+ "detail": f"Request parsing error: "
636
+ f"{str(e)}",
637
+ },
638
+ )
639
+
640
+ return async_pydantic_wrapper
641
+ else:
642
+
643
+ async def sync_pydantic_wrapper(request: Request):
644
+ try:
645
+ body = await request.json()
646
+ parsed_param = param_annotation(**body)
647
+ return handler(parsed_param)
648
+ except Exception as e:
649
+ return JSONResponse(
650
+ status_code=422,
651
+ content={
652
+ "detail": f"Request parsing error: "
653
+ f"{str(e)}",
654
+ },
655
+ )
656
+
657
+ return sync_pydantic_wrapper
658
+
659
+ # For other types, fall back to original behavior
660
+ return handler
661
+
662
+ except Exception:
663
+ # If anything goes wrong with introspection, fall back to
664
+ # original behavior
665
+ return handler
666
+
667
+ @staticmethod
668
+ def _create_streaming_parameter_wrapper(
669
+ handler: Callable,
670
+ is_async_gen: bool = False,
671
+ ):
672
+ """Create a wrapper for streaming handlers that handles parameter
673
+ parsing."""
674
+ try:
675
+ sig = inspect.signature(handler)
676
+ params = list(sig.parameters.values())
677
+ no_params = False
678
+ param_annotation = None
679
+
680
+ if not params:
681
+ no_params = True
682
+ else:
683
+ # Get the first parameter
684
+ first_param = params[0]
685
+ param_annotation = first_param.annotation
686
+
687
+ # If no annotation or annotation is Request, goto no params
688
+ # logic
689
+ if param_annotation in [inspect.Parameter.empty, Request]:
690
+ no_params = True
691
+
692
+ if no_params:
693
+ if is_async_gen:
694
+
695
+ async def async_no_param_wrapper():
696
+ async def generate():
697
+ async for chunk in handler():
698
+ yield str(chunk)
699
+
700
+ return StreamingResponse(
701
+ generate(),
702
+ media_type="text/plain",
703
+ )
704
+
705
+ return async_no_param_wrapper
706
+ else:
707
+
708
+ async def sync_no_param_wrapper():
709
+ def generate():
710
+ for chunk in handler():
711
+ yield str(chunk)
712
+
713
+ return StreamingResponse(
714
+ generate(),
715
+ media_type="text/plain",
716
+ )
717
+
718
+ return sync_no_param_wrapper
719
+
720
+ # Check if the annotation is a Pydantic model
721
+ if isinstance(param_annotation, type) and issubclass(
722
+ param_annotation,
723
+ BaseModel,
724
+ ):
725
+ if is_async_gen:
726
+
727
+ async def async_stream_pydantic_wrapper(
728
+ request: Request,
729
+ ):
730
+ try:
731
+ body = await request.json()
732
+ parsed_param = param_annotation(**body)
733
+
734
+ async def generate():
735
+ async for chunk in handler(parsed_param):
736
+ yield str(chunk)
737
+
738
+ return StreamingResponse(
739
+ generate(),
740
+ media_type="text/plain",
741
+ )
742
+ except Exception as e:
743
+ return StreamingResponse(
744
+ error_stream(e),
745
+ media_type="text/event-stream",
746
+ )
747
+
748
+ return async_stream_pydantic_wrapper
749
+ else:
750
+
751
+ async def sync_stream_pydantic_wrapper(
752
+ request: Request,
753
+ ):
754
+ try:
755
+ body = await request.json()
756
+ parsed_param = param_annotation(**body)
757
+
758
+ def generate():
759
+ for chunk in handler(parsed_param):
760
+ yield str(chunk)
761
+
762
+ return StreamingResponse(
763
+ generate(),
764
+ media_type="text/plain",
765
+ )
766
+ except Exception as e:
767
+ return JSONResponse(
768
+ status_code=422,
769
+ content={
770
+ "detail": f"Request parsing error:"
771
+ f" {str(e)}",
772
+ },
773
+ )
774
+
775
+ return sync_stream_pydantic_wrapper
776
+
777
+ return handler
778
+
779
+ except Exception:
780
+ return handler
781
+
782
+ @staticmethod
783
+ def _add_custom_endpoints(app: FastAPI):
784
+ """Add all custom endpoints to the FastAPI application."""
785
+ if (
786
+ not hasattr(app.state, "custom_endpoints")
787
+ or not app.state.custom_endpoints
788
+ ):
789
+ return
790
+
791
+ for endpoint in app.state.custom_endpoints:
792
+ FastAPIAppFactory._register_single_custom_endpoint(
793
+ app,
794
+ endpoint["path"],
795
+ endpoint["handler"],
796
+ endpoint["methods"],
797
+ endpoint, # Pass the full endpoint config
798
+ )
799
+
800
+ @staticmethod
801
+ def _register_single_custom_endpoint(
802
+ app: FastAPI,
803
+ path: str,
804
+ handler: Callable,
805
+ methods: List[str],
806
+ endpoint_config: Dict = None,
807
+ ):
808
+ """Register a single custom endpoint with proper async/sync
809
+ handling."""
810
+
811
+ for method in methods:
812
+ # Check if this is a task endpoint
813
+ if endpoint_config and endpoint_config.get("task_type"):
814
+ # Create task endpoint with proper execution logic
815
+ task_handler = FastAPIAppFactory._create_task_handler(
816
+ app,
817
+ handler,
818
+ endpoint_config.get("queue", "default"),
819
+ )
820
+ app.add_api_route(path, task_handler, methods=[method])
821
+
822
+ # Add task status endpoint - align with BaseApp pattern
823
+ status_path = f"{path}/{{task_id}}"
824
+ status_handler = FastAPIAppFactory._create_task_status_handler(
825
+ app,
826
+ )
827
+ app.add_api_route(
828
+ status_path,
829
+ status_handler,
830
+ methods=["GET"],
831
+ )
832
+
833
+ else:
834
+ # Regular endpoint handling with automatic parameter parsing
835
+ # Check in the correct order: async gen > sync gen > async &
836
+ # sync
837
+ if inspect.isasyncgenfunction(handler):
838
+ # Async generator -> Streaming response with parameter
839
+ # parsing
840
+ wrapped_handler = (
841
+ FastAPIAppFactory._create_streaming_parameter_wrapper(
842
+ handler,
843
+ is_async_gen=True,
844
+ )
845
+ )
846
+
847
+ app.add_api_route(
848
+ path,
849
+ wrapped_handler,
850
+ methods=[method],
851
+ )
852
+ elif inspect.isgeneratorfunction(handler):
853
+ # Sync generator -> Streaming response with parameter
854
+ # parsing
855
+ wrapped_handler = (
856
+ FastAPIAppFactory._create_streaming_parameter_wrapper(
857
+ handler,
858
+ is_async_gen=False,
859
+ )
860
+ )
861
+ app.add_api_route(
862
+ path,
863
+ wrapped_handler,
864
+ methods=[method],
865
+ )
866
+ else:
867
+ # Sync function -> Async wrapper with parameter parsing
868
+ wrapped_handler = (
869
+ FastAPIAppFactory._create_parameter_wrapper(handler)
870
+ )
871
+ app.add_api_route(path, wrapped_handler, methods=[method])
872
+
873
+ @staticmethod
874
+ def _create_task_handler(app: FastAPI, task_func: Callable, queue: str):
875
+ """Create a task handler that executes functions asynchronously."""
876
+
877
+ async def task_endpoint(request: dict):
878
+ try:
879
+ import uuid
880
+
881
+ # Generate task ID
882
+ task_id = str(uuid.uuid4())
883
+
884
+ # Check if Celery is available
885
+ if (
886
+ hasattr(app.state, "celery_mixin")
887
+ and app.state.celery_mixin
888
+ ):
889
+ # Use Celery for task processing
890
+ celery_mixin = app.state.celery_mixin
891
+
892
+ # Register the task function if not already registered
893
+ if not hasattr(task_func, "celery_task"):
894
+ celery_task = celery_mixin.register_celery_task(
895
+ task_func,
896
+ queue,
897
+ )
898
+ task_func.celery_task = celery_task
899
+
900
+ # Submit task to Celery
901
+ result = celery_mixin.submit_task(task_func, request)
902
+
903
+ return {
904
+ "task_id": result.id,
905
+ "status": "submitted",
906
+ "queue": queue,
907
+ "message": f"Task {result.id} submitted to Celery "
908
+ f"queue {queue}",
909
+ }
910
+
911
+ else:
912
+ # Fallback to in-memory task processing
913
+ import time
914
+
915
+ # Initialize task storage if not exists
916
+ if not hasattr(app.state, "active_tasks"):
917
+ app.state.active_tasks = {}
918
+
919
+ # Create task info for tracking
920
+ task_info = {
921
+ "task_id": task_id,
922
+ "status": "submitted",
923
+ "queue": queue,
924
+ "submitted_at": time.time(),
925
+ "request": request,
926
+ }
927
+ app.state.active_tasks[task_id] = task_info
928
+
929
+ # Execute task asynchronously in background
930
+ asyncio.create_task(
931
+ FastAPIAppFactory._execute_background_task(
932
+ app,
933
+ task_id,
934
+ task_func,
935
+ request,
936
+ queue,
937
+ ),
938
+ )
939
+
940
+ return {
941
+ "task_id": task_id,
942
+ "status": "submitted",
943
+ "queue": queue,
944
+ "message": f"Task {task_id} submitted to queue "
945
+ f"{queue}",
946
+ }
947
+
948
+ except Exception as e:
949
+ return {
950
+ "error": str(e),
951
+ "type": "task",
952
+ "queue": queue,
953
+ "status": "failed",
954
+ }
955
+
956
+ return task_endpoint
957
+
958
+ @staticmethod
959
+ async def _execute_background_task(
960
+ app: FastAPI,
961
+ task_id: str,
962
+ func: Callable,
963
+ request: dict,
964
+ queue: str,
965
+ ):
966
+ """Execute task in background and update status."""
967
+ try:
968
+ import time
969
+ import concurrent.futures
970
+
971
+ # Update status to running
972
+ if (
973
+ hasattr(app.state, "active_tasks")
974
+ and task_id in app.state.active_tasks
975
+ ):
976
+ app.state.active_tasks[task_id].update(
977
+ {
978
+ "status": "running",
979
+ "started_at": time.time(),
980
+ },
981
+ )
982
+
983
+ # Execute the actual task function
984
+ if asyncio.iscoroutinefunction(func):
985
+ result = await func(request)
986
+ else:
987
+ # Run sync function in thread pool to avoid blocking
988
+ with concurrent.futures.ThreadPoolExecutor() as executor:
989
+ result = await asyncio.get_event_loop().run_in_executor(
990
+ executor,
991
+ func,
992
+ request,
993
+ )
994
+
995
+ # Update status to completed
996
+ if (
997
+ hasattr(app.state, "active_tasks")
998
+ and task_id in app.state.active_tasks
999
+ ):
1000
+ app.state.active_tasks[task_id].update(
1001
+ {
1002
+ "status": "completed",
1003
+ "result": result,
1004
+ "completed_at": time.time(),
1005
+ },
1006
+ )
1007
+
1008
+ except Exception as e:
1009
+ # Update status to failed
1010
+ if (
1011
+ hasattr(app.state, "active_tasks")
1012
+ and task_id in app.state.active_tasks
1013
+ ):
1014
+ app.state.active_tasks[task_id].update(
1015
+ {
1016
+ "status": "failed",
1017
+ "error": str(e),
1018
+ "failed_at": time.time(),
1019
+ },
1020
+ )
1021
+
1022
+ @staticmethod
1023
+ def _create_task_status_handler(app: FastAPI):
1024
+ """Create a handler for checking task status."""
1025
+
1026
+ async def task_status_handler(task_id: str):
1027
+ if not task_id:
1028
+ return {"error": "task_id required"}
1029
+
1030
+ # Check if Celery is available
1031
+ if hasattr(app.state, "celery_mixin") and app.state.celery_mixin:
1032
+ # Use Celery for task status checking
1033
+ celery_mixin = app.state.celery_mixin
1034
+ return celery_mixin.get_task_status(task_id)
1035
+
1036
+ else:
1037
+ # Fallback to in-memory task status checking
1038
+ if (
1039
+ not hasattr(app.state, "active_tasks")
1040
+ or task_id not in app.state.active_tasks
1041
+ ):
1042
+ return {"error": f"Task {task_id} not found"}
1043
+
1044
+ task_info = app.state.active_tasks[task_id]
1045
+ task_status = task_info.get("status", "unknown")
1046
+
1047
+ # Align with BaseApp.get_task logic - map internal status to
1048
+ # external status format
1049
+ if task_status in ["submitted", "running"]:
1050
+ return {"status": "pending", "result": None}
1051
+ elif task_status == "completed":
1052
+ return {
1053
+ "status": "finished",
1054
+ "result": task_info.get("result"),
1055
+ }
1056
+ elif task_status == "failed":
1057
+ return {
1058
+ "status": "error",
1059
+ "result": task_info.get("error", "Unknown error"),
1060
+ }
1061
+ else:
1062
+ return {"status": task_status, "result": None}
1063
+
1064
+ return task_status_handler