agentscope-runtime 0.1.5b2__py3-none-any.whl → 0.2.0__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 (107) 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/{sandbox/manager → common}/container_clients/kubernetes_client.py +6 -13
  8. agentscope_runtime/engine/__init__.py +12 -0
  9. agentscope_runtime/engine/agents/agentscope_agent.py +567 -0
  10. agentscope_runtime/engine/agents/agno_agent.py +26 -27
  11. agentscope_runtime/engine/agents/autogen_agent.py +13 -8
  12. agentscope_runtime/engine/agents/langgraph_agent.py +52 -9
  13. agentscope_runtime/engine/agents/utils.py +53 -0
  14. agentscope_runtime/engine/app/__init__.py +6 -0
  15. agentscope_runtime/engine/app/agent_app.py +239 -0
  16. agentscope_runtime/engine/app/base_app.py +181 -0
  17. agentscope_runtime/engine/app/celery_mixin.py +92 -0
  18. agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +5 -1
  19. agentscope_runtime/engine/deployers/base.py +1 -0
  20. agentscope_runtime/engine/deployers/cli_fc_deploy.py +39 -20
  21. agentscope_runtime/engine/deployers/kubernetes_deployer.py +12 -5
  22. agentscope_runtime/engine/deployers/local_deployer.py +61 -3
  23. agentscope_runtime/engine/deployers/modelstudio_deployer.py +201 -40
  24. agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +9 -0
  25. agentscope_runtime/engine/deployers/utils/package_project_utils.py +234 -3
  26. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +567 -7
  27. agentscope_runtime/engine/deployers/utils/service_utils/standalone_main.py.j2 +211 -0
  28. agentscope_runtime/engine/deployers/utils/wheel_packager.py +1 -1
  29. agentscope_runtime/engine/helpers/helper.py +60 -41
  30. agentscope_runtime/engine/runner.py +40 -24
  31. agentscope_runtime/engine/schemas/agent_schemas.py +42 -0
  32. agentscope_runtime/engine/schemas/modelstudio_llm.py +14 -14
  33. agentscope_runtime/engine/services/sandbox_service.py +62 -70
  34. agentscope_runtime/engine/services/tablestore_memory_service.py +307 -0
  35. agentscope_runtime/engine/services/tablestore_rag_service.py +143 -0
  36. agentscope_runtime/engine/services/tablestore_session_history_service.py +293 -0
  37. agentscope_runtime/engine/services/utils/__init__.py +0 -0
  38. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +352 -0
  39. agentscope_runtime/engine/tracing/__init__.py +9 -3
  40. agentscope_runtime/engine/tracing/asyncio_util.py +24 -0
  41. agentscope_runtime/engine/tracing/base.py +66 -34
  42. agentscope_runtime/engine/tracing/local_logging_handler.py +45 -31
  43. agentscope_runtime/engine/tracing/message_util.py +528 -0
  44. agentscope_runtime/engine/tracing/tracing_metric.py +20 -8
  45. agentscope_runtime/engine/tracing/tracing_util.py +130 -0
  46. agentscope_runtime/engine/tracing/wrapper.py +794 -169
  47. agentscope_runtime/sandbox/__init__.py +2 -0
  48. agentscope_runtime/sandbox/box/base/__init__.py +4 -0
  49. agentscope_runtime/sandbox/box/base/base_sandbox.py +6 -4
  50. agentscope_runtime/sandbox/box/browser/__init__.py +4 -0
  51. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +10 -14
  52. agentscope_runtime/sandbox/box/dummy/__init__.py +4 -0
  53. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +2 -1
  54. agentscope_runtime/sandbox/box/filesystem/__init__.py +4 -0
  55. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +10 -7
  56. agentscope_runtime/sandbox/box/gui/__init__.py +4 -0
  57. agentscope_runtime/sandbox/box/gui/box/__init__.py +0 -0
  58. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +81 -0
  59. agentscope_runtime/sandbox/box/sandbox.py +5 -2
  60. agentscope_runtime/sandbox/box/shared/routers/generic.py +20 -1
  61. agentscope_runtime/sandbox/box/training_box/__init__.py +4 -0
  62. agentscope_runtime/sandbox/box/training_box/training_box.py +7 -54
  63. agentscope_runtime/sandbox/build.py +143 -58
  64. agentscope_runtime/sandbox/client/http_client.py +87 -59
  65. agentscope_runtime/sandbox/client/training_client.py +0 -1
  66. agentscope_runtime/sandbox/constant.py +27 -1
  67. agentscope_runtime/sandbox/custom/custom_sandbox.py +7 -6
  68. agentscope_runtime/sandbox/custom/example.py +4 -3
  69. agentscope_runtime/sandbox/enums.py +1 -1
  70. agentscope_runtime/sandbox/manager/sandbox_manager.py +212 -106
  71. agentscope_runtime/sandbox/manager/server/app.py +82 -14
  72. agentscope_runtime/sandbox/manager/server/config.py +50 -3
  73. agentscope_runtime/sandbox/model/container.py +12 -23
  74. agentscope_runtime/sandbox/model/manager_config.py +93 -5
  75. agentscope_runtime/sandbox/registry.py +1 -1
  76. agentscope_runtime/sandbox/tools/gui/__init__.py +7 -0
  77. agentscope_runtime/sandbox/tools/gui/tool.py +77 -0
  78. agentscope_runtime/sandbox/tools/mcp_tool.py +6 -2
  79. agentscope_runtime/sandbox/tools/tool.py +4 -0
  80. agentscope_runtime/sandbox/utils.py +124 -0
  81. agentscope_runtime/version.py +1 -1
  82. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/METADATA +246 -111
  83. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/RECORD +96 -80
  84. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +0 -6
  85. agentscope_runtime/engine/agents/agentscope_agent/agent.py +0 -401
  86. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +0 -169
  87. agentscope_runtime/engine/agents/llm_agent.py +0 -51
  88. agentscope_runtime/engine/llms/__init__.py +0 -3
  89. agentscope_runtime/engine/llms/base_llm.py +0 -60
  90. agentscope_runtime/engine/llms/qwen_llm.py +0 -47
  91. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +0 -22
  92. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +0 -26
  93. agentscope_runtime/sandbox/manager/container_clients/__init__.py +0 -10
  94. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +0 -422
  95. /agentscope_runtime/{sandbox/manager → common}/collections/__init__.py +0 -0
  96. /agentscope_runtime/{sandbox/manager → common}/collections/base_mapping.py +0 -0
  97. /agentscope_runtime/{sandbox/manager → common}/collections/base_queue.py +0 -0
  98. /agentscope_runtime/{sandbox/manager → common}/collections/base_set.py +0 -0
  99. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_queue.py +0 -0
  100. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_set.py +0 -0
  101. /agentscope_runtime/{sandbox/manager → common}/collections/redis_queue.py +0 -0
  102. /agentscope_runtime/{sandbox/manager → common}/collections/redis_set.py +0 -0
  103. /agentscope_runtime/{sandbox/manager → common}/container_clients/base_client.py +0 -0
  104. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/WHEEL +0 -0
  105. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/entry_points.txt +0 -0
  106. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/licenses/LICENSE +0 -0
  107. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/top_level.txt +0 -0
@@ -11,6 +11,7 @@ from autogen_agentchat.messages import (
11
11
  ModelClientStreamingChunkEvent,
12
12
  )
13
13
 
14
+ from .utils import build_agent
14
15
  from ..agents import Agent
15
16
  from ..schemas.context import Context
16
17
  from ..schemas.agent_schemas import (
@@ -136,20 +137,24 @@ class AutogenAgent(Agent):
136
137
  "agent_config": self.agent_config,
137
138
  "agent_builder": agent_builder,
138
139
  }
139
- self._agent = None
140
140
  self.tools = tools
141
141
 
142
142
  def copy(self) -> "AutogenAgent":
143
143
  return AutogenAgent(**self._attr)
144
144
 
145
145
  def build(self, as_context):
146
- self._agent = self._attr["agent_builder"](
146
+ params = {
147
147
  **self._attr["agent_config"],
148
- model_client=as_context.model,
149
- tools=as_context.toolkit,
150
- )
148
+ **{
149
+ "model_client": as_context.model,
150
+ "tools": as_context.toolkit,
151
+ }, # Context will be added at `_agent.run_stream`
152
+ }
153
+
154
+ builder_cls = self._attr["agent_builder"]
155
+ _agent = build_agent(builder_cls, params)
151
156
 
152
- return self._agent
157
+ return _agent
153
158
 
154
159
  async def run(self, context):
155
160
  ag_context = AutogenContextAdapter(context=context, attr=self._attr)
@@ -157,9 +162,9 @@ class AutogenAgent(Agent):
157
162
 
158
163
  # We should always build a new agent since the state is manage outside
159
164
  # the agent
160
- self._agent = self.build(ag_context)
165
+ _agent = self.build(ag_context)
161
166
 
162
- resp = self._agent.run_stream(
167
+ resp = _agent.run_stream(
163
168
  task=ag_context.memory + [ag_context.new_message],
164
169
  )
165
170
 
@@ -3,22 +3,65 @@ import json
3
3
 
4
4
  from langgraph.graph.state import CompiledStateGraph
5
5
 
6
+ from ..schemas.agent_schemas import Message, TextContent
6
7
  from .base_agent import Agent
7
- from ..schemas.agent_schemas import (
8
- Message,
9
- TextContent,
10
- )
11
8
 
12
9
 
13
10
  def _state_folder(messages):
14
- if len(messages) > 0:
15
- return json.loads(messages[0]["content"])
16
- else:
11
+ if not messages or len(messages) == 0:
12
+ # Return empty list if no messages
17
13
  return []
18
14
 
15
+ content = messages[0]["content"]
16
+ role = messages[0]["role"]
17
+
18
+ # If content is a list, extract the text content
19
+ if isinstance(content, list) and len(content) > 0:
20
+ if isinstance(content[0], dict) and content[0].get("type") == "text":
21
+ text_content = content[0].get("text", "")
22
+ else:
23
+ # If not text type, convert to string
24
+ text_content = str(content)
25
+ return {"messages": [{"role": role, "content": text_content}]}
26
+
27
+ # If content is string, parse it as JSON, if failed, return directly
28
+ if isinstance(content, str):
29
+ try:
30
+ return json.loads(content)
31
+ except json.JSONDecodeError:
32
+ # If not valid JSON, return the original string
33
+ return {"messages": [{"role": role, "content": content}]}
34
+
35
+ # If content is already a dictionary, return directly
36
+ if isinstance(content, dict):
37
+ return content
38
+
39
+ # For other cases, wrap in messages and return
40
+ return {"messages": [{"role": role, "content": str(content)}]}
41
+
19
42
 
20
43
  def _state_unfolder(state):
21
- state_jsons = json.dumps(state)
44
+ # Process state that may contain non-serializable objects
45
+ def default_serializer(obj):
46
+ # If object has __dict__ method, use it
47
+ if hasattr(obj, "__dict__"):
48
+ return obj.__dict__
49
+ # If object has model_dump method, use it
50
+ elif hasattr(obj, "model_dump"):
51
+ return obj.model_dump()
52
+ # If object is a message type, extract its content
53
+ elif hasattr(obj, "content"):
54
+ return str(obj.content)
55
+ # For other cases, convert to string
56
+ else:
57
+ return str(obj)
58
+
59
+ # Serialize state to JSON string with custom serializer
60
+ state_jsons = json.dumps(
61
+ state,
62
+ default=default_serializer,
63
+ ensure_ascii=False,
64
+ )
22
65
  return state_jsons
23
66
 
24
67
 
@@ -40,7 +83,7 @@ class LangGraphAgent(Agent):
40
83
  context,
41
84
  **kwargs,
42
85
  ):
43
- # fold the last m
86
+ # Convert messages to list format
44
87
  list_messages = []
45
88
  for m in context.session.messages:
46
89
  dumped = m.model_dump()
@@ -0,0 +1,53 @@
1
+ # -*- coding: utf-8 -*-
2
+ import inspect
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ def build_agent(builder_cls, params):
9
+ """
10
+ Filters out unsupported parameters based on the __init__ signature of
11
+ builder_cls
12
+ and instantiates the class.
13
+
14
+ Args:
15
+ builder_cls (type): The class to instantiate.
16
+ params (dict): Dictionary of parameters to pass to the constructor.
17
+
18
+ Returns:
19
+ object: An instance of builder_cls.
20
+ """
21
+ try:
22
+ # Get the signature of the __init__ method
23
+ sig = inspect.signature(builder_cls.__init__)
24
+ allowed_params = set(sig.parameters.keys())
25
+ # Remove 'self' from the list of allowed parameters
26
+ allowed_params.discard("self")
27
+ except (TypeError, ValueError):
28
+ # If signature cannot be inspected, allow all given params
29
+ allowed_params = set(params.keys())
30
+
31
+ filtered_params = {} # Parameters that are accepted by the constructor
32
+ unsupported = [] # Parameters that are not accepted
33
+
34
+ # Separate supported and unsupported parameters
35
+ for k, v in params.items():
36
+ if k in allowed_params:
37
+ filtered_params[k] = v
38
+ else:
39
+ unsupported.append(f"{k}={v!r}")
40
+
41
+ # Log a warning if there are unsupported parameters
42
+ if unsupported:
43
+ unsupported_str = ", ".join(unsupported)
44
+ logger.warning(
45
+ f"The following parameters are not supported by "
46
+ f"{builder_cls.__name__} and have been ignored: "
47
+ f"{unsupported_str}. If you require these parameters, "
48
+ f"please update the `__init__` method of "
49
+ f"{builder_cls.__name__} to accept and handle them.",
50
+ )
51
+
52
+ # Instantiate the class with only supported parameters
53
+ return builder_cls(**filtered_params)
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .agent_app import AgentApp
3
+
4
+ __all__ = [
5
+ "AgentApp",
6
+ ]
@@ -0,0 +1,239 @@
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio
3
+ import logging
4
+ from contextlib import asynccontextmanager
5
+ from typing import Optional, Any, Callable, List
6
+
7
+ import uvicorn
8
+ from fastapi import FastAPI
9
+ from pydantic import BaseModel
10
+
11
+ from .base_app import BaseApp
12
+ from ..agents.base_agent import Agent
13
+ from ..deployers.adapter.a2a import A2AFastAPIDefaultAdapter
14
+ from ..deployers.adapter.responses.response_api_protocol_adapter import (
15
+ ResponseAPIDefaultAdapter,
16
+ )
17
+ from ..deployers.utils.deployment_modes import DeploymentMode
18
+ from ..deployers.utils.service_utils.fastapi_factory import FastAPIAppFactory
19
+ from ..deployers.utils.service_utils.service_config import (
20
+ DEFAULT_SERVICES_CONFIG,
21
+ )
22
+ from ..runner import Runner
23
+ from ..schemas.agent_schemas import AgentRequest
24
+ from ..services.context_manager import ContextManager
25
+ from ..services.environment_manager import EnvironmentManager
26
+ from ...version import __version__
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class AgentApp(BaseApp):
32
+ """
33
+ The AgentApp class represents an application that runs as an agent.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ *,
39
+ agent: Optional[Agent] = None,
40
+ environment_manager: Optional[EnvironmentManager] = None,
41
+ context_manager: Optional[ContextManager] = None,
42
+ endpoint_path: str = "/process",
43
+ response_type: str = "sse",
44
+ stream: bool = True,
45
+ request_model: Optional[type[BaseModel]] = AgentRequest,
46
+ before_start: Optional[Callable] = None,
47
+ after_finish: Optional[Callable] = None,
48
+ broker_url: Optional[str] = None,
49
+ backend_url: Optional[str] = None,
50
+ **kwargs,
51
+ ):
52
+ """
53
+ Initialize the AgentApp.
54
+
55
+ Args:
56
+ *args: Variable length argument list.
57
+ **kwargs: Arbitrary keyword arguments.
58
+ """
59
+
60
+ self.endpoint_path = endpoint_path
61
+ self.response_type = response_type
62
+ self.stream = stream
63
+ self.request_model = request_model
64
+ self.before_start = before_start
65
+ self.after_finish = after_finish
66
+ self.broker_url = broker_url
67
+ self.backend_url = backend_url
68
+
69
+ self._agent = agent
70
+ self._runner = None
71
+ self.custom_endpoints = [] # Store custom endpoints
72
+
73
+ a2a_protocol = A2AFastAPIDefaultAdapter(agent=self._agent)
74
+ response_protocol = ResponseAPIDefaultAdapter()
75
+ self.protocol_adapters = [a2a_protocol, response_protocol]
76
+
77
+ if self._agent:
78
+ self._runner = Runner(
79
+ agent=self._agent,
80
+ environment_manager=environment_manager,
81
+ context_manager=context_manager,
82
+ )
83
+
84
+ @asynccontextmanager
85
+ async def lifespan(app: FastAPI) -> Any:
86
+ """Manage the application lifespan."""
87
+ if hasattr(self, "before_start") and self.before_start:
88
+ if asyncio.iscoroutinefunction(self.before_start):
89
+ await self.before_start(app, **getattr(self, "kwargs", {}))
90
+ else:
91
+ self.before_start(app, **getattr(self, "kwargs", {}))
92
+ yield
93
+ if hasattr(self, "after_finish") and self.after_finish:
94
+ if asyncio.iscoroutinefunction(self.after_finish):
95
+ await self.after_finish(app, **getattr(self, "kwargs", {}))
96
+ else:
97
+ self.after_finish(app, **getattr(self, "kwargs", {}))
98
+
99
+ kwargs = {
100
+ "title": "Agent Service",
101
+ "version": __version__,
102
+ "description": "Production-ready Agent Service API",
103
+ "lifespan": lifespan,
104
+ **kwargs,
105
+ }
106
+
107
+ if self._runner:
108
+ if self.stream:
109
+ self.func = self._runner.stream_query
110
+ else:
111
+ self.func = self._runner.query
112
+
113
+ super().__init__(
114
+ broker_url=broker_url,
115
+ backend_url=backend_url,
116
+ **kwargs,
117
+ )
118
+
119
+ # Store custom endpoints and tasks for deployment
120
+ # but don't add them to FastAPI here - let FastAPIAppFactory handle it
121
+
122
+ def run(
123
+ self,
124
+ host="0.0.0.0",
125
+ port=8090,
126
+ embed_task_processor=False,
127
+ services_config=None,
128
+ **kwargs,
129
+ ):
130
+ """
131
+ Run the AgentApp using FastAPIAppFactory directly.
132
+
133
+ Args:
134
+ host: Host to bind to
135
+ port: Port to bind to
136
+ embed_task_processor: Whether to embed task processor
137
+ services_config: Optional services configuration
138
+ **kwargs: Additional keyword arguments
139
+ """
140
+
141
+ try:
142
+ logger.info(
143
+ "[AgentApp] Starting AgentApp with FastAPIAppFactory...",
144
+ )
145
+
146
+ # Use default services config if not provided
147
+ if services_config is None:
148
+ services_config = DEFAULT_SERVICES_CONFIG
149
+
150
+ # Create FastAPI application using the factory
151
+ fastapi_app = FastAPIAppFactory.create_app(
152
+ runner=self._runner,
153
+ endpoint_path=self.endpoint_path,
154
+ request_model=self.request_model,
155
+ response_type=self.response_type,
156
+ stream=self.stream,
157
+ before_start=self.before_start,
158
+ after_finish=self.after_finish,
159
+ mode=DeploymentMode.DAEMON_THREAD,
160
+ services_config=services_config,
161
+ protocol_adapters=self.protocol_adapters,
162
+ custom_endpoints=self.custom_endpoints,
163
+ broker_url=self.broker_url,
164
+ backend_url=self.backend_url,
165
+ enable_embedded_worker=embed_task_processor,
166
+ **kwargs,
167
+ )
168
+
169
+ logger.info(f"[AgentApp] Starting server on {host}:{port}")
170
+
171
+ # Start the FastAPI application with uvicorn
172
+ uvicorn.run(
173
+ fastapi_app,
174
+ host=host,
175
+ port=port,
176
+ log_level="info",
177
+ access_log=True,
178
+ )
179
+
180
+ except Exception as e:
181
+ logger.error(f"[AgentApp] Error while running: {e}")
182
+ raise
183
+
184
+ async def deploy(self, deployer, **kwargs):
185
+ """Deploy the agent app with custom endpoints support"""
186
+ # Pass custom endpoints and tasks to the deployer
187
+
188
+ deploy_kwargs = {
189
+ **kwargs,
190
+ "custom_endpoints": self.custom_endpoints,
191
+ "agent": self._agent,
192
+ "runner": self._runner,
193
+ "endpoint_path": self.endpoint_path,
194
+ "stream": self.stream,
195
+ "protocol_adapters": self.protocol_adapters,
196
+ }
197
+ return await deployer.deploy(**deploy_kwargs)
198
+
199
+ def endpoint(self, path: str, methods: Optional[List[str]] = None):
200
+ """Decorator to register custom endpoints"""
201
+
202
+ if methods is None:
203
+ methods = ["POST"]
204
+
205
+ def decorator(func: Callable):
206
+ endpoint_info = {
207
+ "path": path,
208
+ "handler": func,
209
+ "methods": methods,
210
+ "module": getattr(func, "__module__", None),
211
+ "function_name": getattr(func, "__name__", None),
212
+ }
213
+ self.custom_endpoints.append(endpoint_info)
214
+ return func
215
+
216
+ return decorator
217
+
218
+ def task(self, path: str, queue: str = "default"):
219
+ """Decorator to register custom task endpoints"""
220
+
221
+ def decorator(func: Callable):
222
+ # Store task configuration for FastAPIAppFactory to handle
223
+ task_info = {
224
+ "path": path,
225
+ "handler": func, # Store original function
226
+ "methods": ["POST"],
227
+ "module": getattr(func, "__module__", None),
228
+ "function_name": getattr(func, "__name__", None),
229
+ "queue": queue,
230
+ "task_type": True, # Mark as task endpoint
231
+ "original_func": func,
232
+ }
233
+ self.custom_endpoints.append(
234
+ task_info,
235
+ ) # Add to endpoints for deployment
236
+
237
+ return func
238
+
239
+ return decorator
@@ -0,0 +1,181 @@
1
+ # -*- coding: utf-8 -*-
2
+ import inspect
3
+ import logging
4
+ import threading
5
+ from typing import Callable, Optional
6
+
7
+ import uvicorn
8
+ from fastapi import FastAPI, Request
9
+ from fastapi.responses import StreamingResponse
10
+
11
+ from .celery_mixin import CeleryMixin
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class BaseApp(FastAPI, CeleryMixin):
17
+ """
18
+ BaseApp extends FastAPI and integrates with Celery
19
+ for asynchronous background task execution.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ broker_url: Optional[str] = None,
25
+ backend_url: Optional[str] = None,
26
+ **kwargs,
27
+ ):
28
+ # Initialize CeleryMixin
29
+ CeleryMixin.__init__(self, broker_url, backend_url)
30
+
31
+ self.server = None
32
+
33
+ # Initialize FastAPI
34
+ FastAPI.__init__(self, **kwargs)
35
+
36
+ def task(self, path: str, queue: str = "celery"):
37
+ """
38
+ Register an asynchronous task endpoint.
39
+ POST <path> -> Create a task and return task ID
40
+ GET <path>/{task_id} -> Check the task status and result
41
+ Combines Celery and FastAPI routing functionality.
42
+ """
43
+ if self.celery_app is None:
44
+ raise RuntimeError(
45
+ f"[AgentApp] Cannot register task endpoint '{path}'.\n"
46
+ f"Reason: The @task decorator requires a background task "
47
+ f"queue to run asynchronous jobs.\n\n"
48
+ "If you want to use async task queue, you must initialize "
49
+ "AgentApp with broker_url and backend_url, e.g.: \n\n"
50
+ " app = AgentApp(\n"
51
+ " broker_url='redis://localhost:6379/0',\n"
52
+ " backend_url='redis://localhost:6379/0'\n"
53
+ " )\n",
54
+ )
55
+
56
+ def decorator(func: Callable):
57
+ # Register Celery task using CeleryMixin
58
+ celery_task = self.register_celery_task(func, queue=queue)
59
+
60
+ # Add FastAPI HTTP routes
61
+ @self.post(path)
62
+ async def create_task(request: Request):
63
+ if len(inspect.signature(func).parameters) > 0:
64
+ body = await request.json()
65
+ task = celery_task.delay(body)
66
+ else:
67
+ task = celery_task.delay()
68
+ return {"task_id": task.id}
69
+
70
+ @self.get(path + "/{task_id}")
71
+ async def get_task(task_id: str):
72
+ return self.get_task_status(task_id)
73
+
74
+ return func
75
+
76
+ return decorator
77
+
78
+ def endpoint(self, path: str):
79
+ """
80
+ Unified POST endpoint decorator.
81
+ Pure FastAPI functionality, independent of Celery.
82
+ Supports:
83
+ - Sync functions
84
+ - Async functions (coroutines)
85
+ - Sync/async generator functions (streaming responses)
86
+ """
87
+
88
+ def decorator(func: Callable):
89
+ is_async_gen = inspect.isasyncgenfunction(func)
90
+ is_sync_gen = inspect.isgeneratorfunction(func)
91
+
92
+ if is_async_gen or is_sync_gen:
93
+ # Handle streaming responses
94
+ async def _stream_generator(request: Request):
95
+ if is_async_gen:
96
+ async for chunk in func(request):
97
+ yield chunk
98
+ else:
99
+ for chunk in func(request):
100
+ yield chunk
101
+
102
+ @self.post(path)
103
+ async def _wrapped(request: Request):
104
+ return StreamingResponse(
105
+ _stream_generator(request),
106
+ media_type="text/plain",
107
+ )
108
+
109
+ else:
110
+ # Handle regular responses
111
+ @self.post(path)
112
+ async def _wrapped(request: Request):
113
+ if inspect.iscoroutinefunction(func):
114
+ return await func(request)
115
+ else:
116
+ return func(request)
117
+
118
+ return func
119
+
120
+ return decorator
121
+
122
+ def run(
123
+ self,
124
+ host="0.0.0.0",
125
+ port=8090,
126
+ embed_task_processor=False,
127
+ **kwargs,
128
+ ):
129
+ """
130
+ Run FastAPI with uvicorn.
131
+ """
132
+ if embed_task_processor:
133
+ if self.celery_app is None:
134
+ logger.warning(
135
+ "[AgentApp] Celery is not configured. "
136
+ "Cannot run embedded worker.",
137
+ )
138
+ else:
139
+ logger.warning(
140
+ "[AgentApp] embed_task_processor=True: Running "
141
+ "task_processor in embedded thread mode. This is "
142
+ "intended for development/debug purposes only. In "
143
+ "production, run Celery worker in a separate process!",
144
+ )
145
+
146
+ queues = self._registered_queues or {"celery"}
147
+ queue_list = ",".join(sorted(queues))
148
+
149
+ def start_celery_worker():
150
+ logger.info(
151
+ f"[AgentApp] Embedded worker listening "
152
+ f"queues: {queue_list}",
153
+ )
154
+ self.celery_app.worker_main(
155
+ [
156
+ "worker",
157
+ "--loglevel=INFO",
158
+ "-Q",
159
+ queue_list,
160
+ ],
161
+ )
162
+
163
+ threading.Thread(
164
+ target=start_celery_worker,
165
+ daemon=True,
166
+ ).start()
167
+ logger.info(
168
+ "[AgentApp] Embedded task processor started in background "
169
+ "thread (DEV mode).",
170
+ )
171
+
172
+ # TODO: Add CLI to main entrypoint to control run/deploy
173
+
174
+ config = uvicorn.Config(
175
+ app=self,
176
+ host=host,
177
+ port=port,
178
+ **kwargs,
179
+ )
180
+ self.server = uvicorn.Server(config)
181
+ self.server.run()