agentscope-runtime 0.1.5b1__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 (106) 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 +72 -12
  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 +77 -27
  21. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +3 -3
  22. agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +9 -0
  23. agentscope_runtime/engine/deployers/utils/package_project_utils.py +234 -3
  24. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +567 -7
  25. agentscope_runtime/engine/deployers/utils/service_utils/standalone_main.py.j2 +211 -0
  26. agentscope_runtime/engine/deployers/utils/wheel_packager.py +1 -1
  27. agentscope_runtime/engine/helpers/helper.py +60 -41
  28. agentscope_runtime/engine/runner.py +35 -24
  29. agentscope_runtime/engine/schemas/agent_schemas.py +42 -0
  30. agentscope_runtime/engine/schemas/modelstudio_llm.py +14 -14
  31. agentscope_runtime/engine/services/sandbox_service.py +62 -70
  32. agentscope_runtime/engine/services/tablestore_memory_service.py +304 -0
  33. agentscope_runtime/engine/services/tablestore_rag_service.py +143 -0
  34. agentscope_runtime/engine/services/tablestore_session_history_service.py +293 -0
  35. agentscope_runtime/engine/services/utils/__init__.py +0 -0
  36. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +352 -0
  37. agentscope_runtime/engine/tracing/__init__.py +9 -3
  38. agentscope_runtime/engine/tracing/asyncio_util.py +24 -0
  39. agentscope_runtime/engine/tracing/base.py +66 -34
  40. agentscope_runtime/engine/tracing/local_logging_handler.py +45 -31
  41. agentscope_runtime/engine/tracing/message_util.py +528 -0
  42. agentscope_runtime/engine/tracing/tracing_metric.py +20 -8
  43. agentscope_runtime/engine/tracing/tracing_util.py +130 -0
  44. agentscope_runtime/engine/tracing/wrapper.py +794 -169
  45. agentscope_runtime/sandbox/__init__.py +2 -0
  46. agentscope_runtime/sandbox/box/base/__init__.py +4 -0
  47. agentscope_runtime/sandbox/box/base/base_sandbox.py +6 -4
  48. agentscope_runtime/sandbox/box/browser/__init__.py +4 -0
  49. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +10 -14
  50. agentscope_runtime/sandbox/box/dummy/__init__.py +4 -0
  51. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +2 -1
  52. agentscope_runtime/sandbox/box/filesystem/__init__.py +4 -0
  53. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +10 -7
  54. agentscope_runtime/sandbox/box/gui/__init__.py +4 -0
  55. agentscope_runtime/sandbox/box/gui/box/__init__.py +0 -0
  56. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +81 -0
  57. agentscope_runtime/sandbox/box/sandbox.py +5 -2
  58. agentscope_runtime/sandbox/box/shared/routers/generic.py +20 -1
  59. agentscope_runtime/sandbox/box/training_box/__init__.py +4 -0
  60. agentscope_runtime/sandbox/box/training_box/training_box.py +10 -15
  61. agentscope_runtime/sandbox/build.py +143 -58
  62. agentscope_runtime/sandbox/client/http_client.py +87 -59
  63. agentscope_runtime/sandbox/client/training_client.py +0 -1
  64. agentscope_runtime/sandbox/constant.py +27 -1
  65. agentscope_runtime/sandbox/custom/custom_sandbox.py +7 -6
  66. agentscope_runtime/sandbox/custom/example.py +4 -3
  67. agentscope_runtime/sandbox/enums.py +1 -0
  68. agentscope_runtime/sandbox/manager/sandbox_manager.py +212 -106
  69. agentscope_runtime/sandbox/manager/server/app.py +82 -14
  70. agentscope_runtime/sandbox/manager/server/config.py +50 -3
  71. agentscope_runtime/sandbox/model/container.py +12 -23
  72. agentscope_runtime/sandbox/model/manager_config.py +93 -5
  73. agentscope_runtime/sandbox/registry.py +1 -1
  74. agentscope_runtime/sandbox/tools/gui/__init__.py +7 -0
  75. agentscope_runtime/sandbox/tools/gui/tool.py +77 -0
  76. agentscope_runtime/sandbox/tools/mcp_tool.py +6 -2
  77. agentscope_runtime/sandbox/tools/tool.py +4 -0
  78. agentscope_runtime/sandbox/utils.py +124 -0
  79. agentscope_runtime/version.py +1 -1
  80. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.2.0b1.dist-info}/METADATA +209 -101
  81. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.2.0b1.dist-info}/RECORD +95 -79
  82. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +0 -6
  83. agentscope_runtime/engine/agents/agentscope_agent/agent.py +0 -401
  84. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +0 -169
  85. agentscope_runtime/engine/agents/llm_agent.py +0 -51
  86. agentscope_runtime/engine/llms/__init__.py +0 -3
  87. agentscope_runtime/engine/llms/base_llm.py +0 -60
  88. agentscope_runtime/engine/llms/qwen_llm.py +0 -47
  89. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +0 -22
  90. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +0 -26
  91. agentscope_runtime/sandbox/manager/container_clients/__init__.py +0 -10
  92. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +0 -422
  93. /agentscope_runtime/{sandbox/manager → common}/collections/__init__.py +0 -0
  94. /agentscope_runtime/{sandbox/manager → common}/collections/base_mapping.py +0 -0
  95. /agentscope_runtime/{sandbox/manager → common}/collections/base_queue.py +0 -0
  96. /agentscope_runtime/{sandbox/manager → common}/collections/base_set.py +0 -0
  97. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_queue.py +0 -0
  98. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_set.py +0 -0
  99. /agentscope_runtime/{sandbox/manager → common}/collections/redis_queue.py +0 -0
  100. /agentscope_runtime/{sandbox/manager → common}/collections/redis_set.py +0 -0
  101. /agentscope_runtime/{sandbox/manager → common}/container_clients/base_client.py +0 -0
  102. /agentscope_runtime/{sandbox/manager → common}/container_clients/kubernetes_client.py +0 -0
  103. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.2.0b1.dist-info}/WHEEL +0 -0
  104. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.2.0b1.dist-info}/entry_points.txt +0 -0
  105. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.2.0b1.dist-info}/licenses/LICENSE +0 -0
  106. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.2.0b1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,16 @@
1
1
  # -*- coding: utf-8 -*-
2
- # pylint:disable=too-many-statements,typevar-name-incorrect-variance
2
+ # type: ignore
3
+
4
+ import contextvars
3
5
  import inspect
6
+ import json
7
+ import os
8
+ import re
9
+ import time
10
+ import uuid
11
+ from collections.abc import Callable
12
+ from copy import deepcopy
13
+ from enum import Enum
4
14
  from functools import wraps
5
15
  from typing import (
6
16
  Any,
@@ -8,59 +18,101 @@ from typing import (
8
18
  Dict,
9
19
  Iterable,
10
20
  Optional,
21
+ TypeVar,
11
22
  Union,
12
23
  )
13
- from typing import AsyncIterable, AsyncIterator, Tuple, TypeVar
14
24
 
15
25
  from pydantic import BaseModel
16
- from openai.types.chat import ChatCompletionChunk
26
+ from opentelemetry.propagate import extract
27
+ from opentelemetry.context import attach
28
+ from opentelemetry.trace import StatusCode
29
+ from opentelemetry import trace as ot_trace
30
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
31
+ OTLPSpanExporter as OTLPSpanGrpcExporter,
32
+ )
33
+ from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource
34
+ from opentelemetry.sdk.trace import TracerProvider
35
+ from opentelemetry.sdk.trace.export import (
36
+ BatchSpanProcessor,
37
+ ConsoleSpanExporter,
38
+ )
39
+
40
+ from .asyncio_util import aenumerate
41
+ from .message_util import (
42
+ merge_incremental_chunk,
43
+ get_finish_reason,
44
+ )
17
45
 
18
- from .base import Tracer, TraceType
46
+ from .base import Tracer, TracerHandler, EventContext
47
+ from .tracing_metric import TraceType
19
48
  from .local_logging_handler import LocalLogHandler
49
+ from .tracing_util import TracingUtil
20
50
 
21
- T = TypeVar("T", covariant=True)
51
+ T_co = TypeVar("T_co", covariant=True)
22
52
 
23
53
 
24
- async def aenumerate(
25
- asequence: AsyncIterable[T],
26
- start: int = 0,
27
- ) -> AsyncIterator[Tuple[int, T]]:
28
- """Asynchronously enumerate an async iterator from a given start value.
54
+ def _str_to_bool(value: str) -> bool:
55
+ """Convert string to boolean value.
29
56
 
30
57
  Args:
31
- asequence (AsyncIterable[T]): The async iterable to enumerate.
32
- start (int): The starting value for enumeration. Defaults to 0.
58
+ value (str): String value to convert.
33
59
 
34
- Yields:
35
- Tuple[int, T]: A tuple containing the index and the item from the
36
- async iterable.
60
+ Returns:
61
+ bool: Boolean representation of the string.
37
62
  """
38
- n = start
39
- async for elem in asequence:
40
- yield n, elem
41
- n += 1
63
+ return value.lower() in ("true", "1", "yes", "on")
64
+
65
+
66
+ class MineType(str, Enum):
67
+ """MIME type enumeration for content types."""
68
+
69
+ TEXT = "text/plain"
70
+ JSON = "application/json"
71
+
72
+
73
+ _parent_span_context: contextvars.ContextVar = contextvars.ContextVar(
74
+ "_parent_span_context",
75
+ default=None,
76
+ )
77
+
78
+ _current_request_id: contextvars.ContextVar[str] = contextvars.ContextVar(
79
+ "_current_request_id",
80
+ default="",
81
+ )
82
+
83
+ _current_trace_header: contextvars.ContextVar[dict] = contextvars.ContextVar(
84
+ "_current_trace_header",
85
+ default={},
86
+ )
42
87
 
43
88
 
44
- def trace(
45
- trace_type: Union[TraceType, str],
46
- tracer: Optional[Tracer] = Tracer(
47
- handlers=[LocalLogHandler(enable_console=True)],
48
- ),
89
+ def trace( # pylint: disable=too-many-statements
90
+ trace_type: Union[TraceType, str, None] = None,
91
+ *,
92
+ trace_name: Optional[str] = None,
93
+ is_root_span: Optional[bool] = None,
94
+ get_finish_reason_func: Optional[
95
+ Callable[[Any], Optional[str]]
96
+ ] = get_finish_reason,
97
+ merge_output_func: Optional[
98
+ Callable[[Any], Union[BaseModel, dict, str, None]]
99
+ ] = merge_incremental_chunk,
49
100
  ) -> Any:
50
101
  """Decorator for tracing function execution.
51
102
 
52
103
  Args:
53
104
  trace_type (Union[TraceType, str]): The type of trace event.
54
- tracer (Optional[Tracer]): The tracer instance to use. Defaults to
55
- a new Tracer with LocalLogHandler.
105
+ trace_name (Optional[str]): The name of trace event.
106
+ is_root_span (Optional[bool]): Specify current span as root span
107
+ get_finish_reason_func(Optional[Callable]): The function to judge
108
+ if stopped
109
+ merge_output_func(Optional[Callable]): The function to merge outputs
56
110
 
57
111
  Returns:
58
112
  Any: The decorated function with tracing capabilities.
59
113
  """
60
- if isinstance(trace_type, str):
61
- trace_type = TraceType(trace_type)
62
114
 
63
- def task_wrapper(func: Any) -> Any:
115
+ def wrapper(func: Any) -> Any:
64
116
  """Wrapper function that applies tracing to the target function.
65
117
 
66
118
  Args:
@@ -70,6 +122,7 @@ def trace(
70
122
  Any: The wrapped function with appropriate tracing logic.
71
123
  """
72
124
 
125
+ @wraps(func)
73
126
  async def async_exec(*args: Any, **kwargs: Any) -> Any:
74
127
  """Execute async function with tracing.
75
128
 
@@ -80,18 +133,99 @@ def trace(
80
133
  Returns:
81
134
  Any: The result of the traced function.
82
135
  """
83
- start_payload = _get_start_payload(args, kwargs)
84
- with tracer.event(
136
+
137
+ _init_trace_context()
138
+
139
+ start_payload = _get_start_payload(args, kwargs, func)
140
+
141
+ trace_context = kwargs.get("trace_context") if kwargs else None
142
+
143
+ if trace_context:
144
+ attach(trace_context)
145
+
146
+ parent_ctx = (
147
+ trace_context
148
+ if trace_context
149
+ else _parent_span_context.get(None)
150
+ )
151
+
152
+ (
153
+ final_trace_type,
154
+ final_trace_name,
155
+ final_is_root_span,
156
+ ) = _validate_trace_options(
85
157
  trace_type,
86
- payload=start_payload,
87
- **kwargs,
88
- ) as event:
89
- kwargs = kwargs if kwargs is not None else {}
90
- kwargs["trace_event"] = event
91
- result = await func(*args, **kwargs)
92
- event.on_end(payload=_obj_to_dict(result))
93
- return result
158
+ trace_name,
159
+ is_root_span,
160
+ func.__name__,
161
+ parent_ctx,
162
+ )
163
+
164
+ # Auto generate request_id for root span if needed
165
+ _set_request_id(parent_ctx)
166
+
167
+ common_attrs = TracingUtil.get_common_attributes() or {}
168
+
169
+ span_attributes = {
170
+ "gen_ai.span.kind": final_trace_type,
171
+ "gen_ai.user.query_root_flag": 1 if final_is_root_span else 0,
172
+ "input.mine_type": MineType.JSON,
173
+ "input.value": json.dumps(
174
+ start_payload,
175
+ ensure_ascii=False,
176
+ ),
177
+ **common_attrs,
178
+ }
179
+
180
+ with _ot_tracer.start_as_current_span(
181
+ final_trace_name,
182
+ context=parent_ctx,
183
+ attributes=span_attributes,
184
+ ) as span:
185
+ span.set_status(status=StatusCode.OK)
186
+ with _tracer.event(
187
+ span,
188
+ final_trace_name,
189
+ payload=start_payload,
190
+ ) as event:
191
+ _parent_span_context.set(
192
+ ot_trace.set_span_in_context(span),
193
+ )
194
+ if _function_accepts_kwargs(func):
195
+ func_kwargs = kwargs.copy() if kwargs else {}
196
+ func_kwargs["trace_event"] = event
197
+ else:
198
+ func_kwargs = kwargs.copy() if kwargs else {}
199
+
200
+ try:
201
+ result = await func(*args, **func_kwargs)
202
+ end_payload = _obj_to_dict(result)
203
+ (
204
+ output_mine_type,
205
+ output_value,
206
+ ) = _get_ot_type_and_value(end_payload)
207
+ span.set_attribute(
208
+ "output.mine_type",
209
+ output_mine_type,
210
+ )
211
+ span.set_attribute(
212
+ "output.value",
213
+ output_value,
214
+ )
215
+ event.on_end(payload=end_payload)
216
+ return result
217
+ except Exception as e:
218
+ span.set_status(
219
+ status=StatusCode.ERROR,
220
+ description=f"exception={e}",
221
+ )
222
+ event.on_log(str(e))
223
+ raise e
224
+ finally:
225
+ if not trace_context:
226
+ _parent_span_context.set(parent_ctx)
94
227
 
228
+ @wraps(func)
95
229
  def sync_exec(*args: Any, **kwargs: Any) -> Any:
96
230
  """Execute sync function with tracing.
97
231
 
@@ -102,23 +236,103 @@ def trace(
102
236
  Returns:
103
237
  Any: The result of the traced function.
104
238
  """
105
- start_payload = _get_start_payload(args, kwargs)
106
- with tracer.event(
239
+
240
+ _init_trace_context()
241
+
242
+ start_payload = _get_start_payload(args, kwargs, func)
243
+
244
+ trace_context = kwargs.get("trace_context") if kwargs else None
245
+
246
+ if trace_context:
247
+ attach(trace_context)
248
+
249
+ parent_ctx = (
250
+ trace_context
251
+ if trace_context
252
+ else _parent_span_context.get(None)
253
+ )
254
+
255
+ (
256
+ final_trace_type,
257
+ final_trace_name,
258
+ final_is_root_span,
259
+ ) = _validate_trace_options(
107
260
  trace_type,
108
- payload=start_payload,
109
- **kwargs,
110
- ) as event:
111
- kwargs = kwargs if kwargs is not None else {}
112
- kwargs["trace_event"] = event
113
- result = func(*args, **kwargs)
114
- event.on_end(payload=_obj_to_dict(result))
115
- return result
261
+ trace_name,
262
+ is_root_span,
263
+ func.__name__,
264
+ parent_ctx,
265
+ )
266
+
267
+ # Auto generate request_id for root span if needed
268
+ _set_request_id(parent_ctx)
269
+
270
+ common_attrs = TracingUtil.get_common_attributes() or {}
271
+
272
+ span_attributes = {
273
+ "gen_ai.span.kind": final_trace_type,
274
+ "gen_ai.user.query_root_flag": 1 if final_is_root_span else 0,
275
+ "input.mine_type": MineType.JSON,
276
+ "input.value": json.dumps(
277
+ start_payload,
278
+ ensure_ascii=False,
279
+ ),
280
+ **common_attrs,
281
+ }
282
+
283
+ with _ot_tracer.start_as_current_span(
284
+ final_trace_name,
285
+ context=parent_ctx,
286
+ attributes=span_attributes,
287
+ ) as span:
288
+ span.set_status(status=StatusCode.OK)
289
+ with _tracer.event(
290
+ span,
291
+ final_trace_name,
292
+ payload=start_payload,
293
+ ) as event:
294
+ _parent_span_context.set(
295
+ ot_trace.set_span_in_context(span),
296
+ )
297
+ if _function_accepts_kwargs(func):
298
+ func_kwargs = kwargs.copy() if kwargs else {}
299
+ func_kwargs["trace_event"] = event
300
+ else:
301
+ func_kwargs = kwargs.copy() if kwargs else {}
302
+
303
+ try:
304
+ result = func(*args, **func_kwargs)
305
+ end_payload = _obj_to_dict(result)
306
+ (
307
+ output_mine_type,
308
+ output_value,
309
+ ) = _get_ot_type_and_value(end_payload)
310
+ span.set_attribute(
311
+ "output.mine_type",
312
+ output_mine_type,
313
+ )
314
+ span.set_attribute(
315
+ "output.value",
316
+ output_value,
317
+ )
318
+ event.on_end(payload=end_payload)
319
+ return result
320
+ except Exception as e:
321
+ span.set_status(
322
+ status=StatusCode.ERROR,
323
+ description=f"exception={e}",
324
+ )
325
+ event.on_log(str(e))
326
+ raise e
327
+ finally:
328
+ if not trace_context:
329
+ _parent_span_context.set(parent_ctx)
116
330
 
117
331
  @wraps(func)
118
- async def async_iter_task(
332
+ async def async_iter_task( # pylint: disable=too-many-statements
119
333
  *args: Any,
120
334
  **kwargs: Any,
121
- ) -> AsyncGenerator[T, None]:
335
+ ) -> AsyncGenerator[T_co, None]:
122
336
  """Execute async generator function with tracing.
123
337
 
124
338
  Args:
@@ -126,76 +340,132 @@ def trace(
126
340
  **kwargs (Any): Keyword arguments for the function.
127
341
 
128
342
  Yields:
129
- T: Items from the traced async generator.
343
+ T_co: Items from the original generator with tracing.
130
344
  """
131
- start_payload = _get_start_payload(args, kwargs)
132
- with tracer.event(
345
+ _init_trace_context()
346
+
347
+ start_payload = _get_start_payload(args, kwargs, func)
348
+
349
+ trace_context = kwargs.get("trace_context") if kwargs else None
350
+
351
+ if trace_context:
352
+ attach(trace_context)
353
+
354
+ parent_ctx = (
355
+ trace_context
356
+ if trace_context
357
+ else _parent_span_context.get(None)
358
+ )
359
+
360
+ (
361
+ final_trace_type,
362
+ final_trace_name,
363
+ final_is_root_span,
364
+ ) = _validate_trace_options(
133
365
  trace_type,
134
- payload=start_payload,
135
- **kwargs,
136
- ) as event:
137
- kwargs = kwargs if kwargs is not None else {}
138
- kwargs["trace_event"] = event
139
- cumulated = []
140
-
141
- async def iter_entry() -> AsyncGenerator[T, None]:
142
- """Internal async generator for processing items.
143
-
144
- Yields:
145
- T: Items from the original generator with tracing.
146
- """
147
- try:
148
- async for i, resp in aenumerate(
149
- func(*args, **kwargs),
150
- ): # type: ignore
151
- if i == 0:
152
- event.on_log(
153
- "",
154
- **{
155
- "step_suffix": "first_resp",
156
- "payload": resp.model_dump(),
157
- },
366
+ trace_name,
367
+ is_root_span,
368
+ func.__name__,
369
+ parent_ctx,
370
+ )
371
+
372
+ # Auto generate request_id for root span if needed
373
+ _set_request_id(parent_ctx)
374
+
375
+ common_attrs = TracingUtil.get_common_attributes() or {}
376
+
377
+ span_attributes = {
378
+ "gen_ai.span.kind": final_trace_type,
379
+ "gen_ai.user.query_root_flag": 1 if final_is_root_span else 0,
380
+ "input.mine_type": MineType.JSON,
381
+ "input.value": json.dumps(
382
+ start_payload,
383
+ ensure_ascii=False,
384
+ ),
385
+ **common_attrs,
386
+ }
387
+
388
+ with _ot_tracer.start_as_current_span(
389
+ final_trace_name,
390
+ context=parent_ctx,
391
+ attributes=span_attributes,
392
+ ) as span:
393
+ span.set_status(status=StatusCode.OK)
394
+ with _tracer.event(
395
+ span,
396
+ final_trace_name,
397
+ payload=start_payload,
398
+ ) as event:
399
+ _parent_span_context.set(
400
+ ot_trace.set_span_in_context(span),
401
+ )
402
+ if _function_accepts_kwargs(func):
403
+ func_kwargs = kwargs.copy() if kwargs else {}
404
+ func_kwargs["trace_event"] = event
405
+ else:
406
+ func_kwargs = kwargs.copy() if kwargs else {}
407
+
408
+ cumulated = []
409
+
410
+ async def iter_entry() -> AsyncGenerator[T_co, None]:
411
+ """Internal async generator for processing items.
412
+
413
+ Yields:
414
+ T_co: Items from the original generator with
415
+ tracing.
416
+ """
417
+ try:
418
+ start_time = int(time.time() * 1000)
419
+ async for i, resp in aenumerate(
420
+ func(*args, **func_kwargs),
421
+ ): # type: ignore
422
+ yield resp
423
+ cumulated.append(resp)
424
+
425
+ if i == 0:
426
+ _trace_first_resp(
427
+ resp,
428
+ event,
429
+ span,
430
+ start_time,
431
+ )
432
+
433
+ if get_finish_reason_func is not None:
434
+ _trace_last_resp(
435
+ resp,
436
+ get_finish_reason_func,
437
+ event,
438
+ span,
439
+ )
440
+
441
+ if cumulated and merge_output_func is not None:
442
+ _trace_merged_resp(
443
+ cumulated,
444
+ merge_output_func,
445
+ event,
446
+ span,
158
447
  )
159
- # todo: support more components
160
- if isinstance(resp, ChatCompletionChunk):
161
- if len(resp.choices) > 0:
162
- cumulated.append(resp)
163
- if (
164
- resp.choices[0].finish_reason
165
- is not None
166
- ):
167
- if (
168
- resp.choices[0].finish_reason
169
- == "stop"
170
- ):
171
- step_suffix = "last_resp"
172
- else:
173
- step_suffix = resp.choices[
174
- 0
175
- ].finish_reason
176
- event.on_log(
177
- "",
178
- **{
179
- "step_suffix": step_suffix,
180
- "payload": resp.model_dump(),
181
- },
182
- )
183
- elif resp.usage:
184
- cumulated.append(resp)
185
448
 
449
+ except Exception as e:
450
+ span.set_status(
451
+ status=StatusCode.ERROR,
452
+ description=f"exception={e}",
453
+ )
454
+ event.on_log(str(e))
455
+ raise e
456
+ finally:
457
+ if not trace_context:
458
+ _parent_span_context.set(parent_ctx)
459
+
460
+ try:
461
+ async for resp in iter_entry():
186
462
  yield resp
463
+
187
464
  except Exception as e:
188
465
  raise e
189
466
 
190
- try:
191
- async for resp in iter_entry():
192
- yield resp
193
-
194
- except Exception as e:
195
- raise e
196
-
197
467
  @wraps(func)
198
- def iter_task(*args: Any, **kwargs: Any) -> Iterable[T]:
468
+ def iter_task(*args: Any, **kwargs: Any) -> Iterable[T_co]:
199
469
  """Execute generator function with tracing.
200
470
 
201
471
  Args:
@@ -203,84 +473,175 @@ def trace(
203
473
  **kwargs (Any): Keyword arguments for the function.
204
474
 
205
475
  Yields:
206
- T: Items from the traced generator.
476
+ T_co: Items from the traced generator.
207
477
  """
208
- start_payload = _get_start_payload(args, kwargs)
209
- with tracer.event(
478
+ _init_trace_context()
479
+
480
+ start_payload = _get_start_payload(args, kwargs, func)
481
+
482
+ trace_context = kwargs.get("trace_context") if kwargs else None
483
+
484
+ if trace_context:
485
+ attach(trace_context)
486
+
487
+ parent_ctx = (
488
+ trace_context
489
+ if trace_context
490
+ else _parent_span_context.get(None)
491
+ )
492
+
493
+ (
494
+ final_trace_type,
495
+ final_trace_name,
496
+ final_is_root_span,
497
+ ) = _validate_trace_options(
210
498
  trace_type,
211
- payload=start_payload,
212
- **kwargs,
213
- ) as event:
214
- cumulated = []
215
- try:
216
- kwargs = kwargs if kwargs is not None else {}
217
- kwargs["trace_event"] = event
218
- for i, resp in enumerate(func(*args, **kwargs)):
219
- if i == 0:
220
- event.on_log(
221
- "",
222
- **{
223
- "step_suffix": "first_resp",
224
- "payload": resp.model_dump(),
225
- },
226
- )
227
- # todo: support more components
228
- if len(resp.choices) > 0:
499
+ trace_name,
500
+ is_root_span,
501
+ func.__name__,
502
+ parent_ctx,
503
+ )
504
+
505
+ # Auto generate request_id for root span if needed
506
+ _set_request_id(parent_ctx)
507
+
508
+ common_attrs = TracingUtil.get_common_attributes() or {}
509
+
510
+ span_attributes = {
511
+ "gen_ai.span.kind": final_trace_type,
512
+ "gen_ai.user.query_root_flag": 1 if final_is_root_span else 0,
513
+ "input.mine_type": MineType.JSON,
514
+ "input.value": json.dumps(
515
+ start_payload,
516
+ ensure_ascii=False,
517
+ ),
518
+ **common_attrs,
519
+ }
520
+
521
+ with _ot_tracer.start_as_current_span(
522
+ final_trace_name,
523
+ context=parent_ctx,
524
+ attributes=span_attributes,
525
+ ) as span:
526
+ span.set_status(status=StatusCode.OK)
527
+ with _tracer.event(
528
+ span,
529
+ final_trace_name,
530
+ payload=start_payload,
531
+ ) as event:
532
+ _parent_span_context.set(
533
+ ot_trace.set_span_in_context(span),
534
+ )
535
+ try:
536
+ if _function_accepts_kwargs(func):
537
+ func_kwargs = kwargs.copy() if kwargs else {}
538
+ func_kwargs["trace_event"] = event
539
+ else:
540
+ func_kwargs = kwargs.copy() if kwargs else {}
541
+
542
+ cumulated = []
543
+ start_time = int(time.time() * 1000)
544
+ for i, resp in enumerate(func(*args, **func_kwargs)):
545
+ yield resp
229
546
  cumulated.append(resp)
230
- if resp.choices[0].finish_reason is not None:
231
- if resp.choices[0].finish_reason == "stop":
232
- step_suffix = "last_resp"
233
- else:
234
- step_suffix = resp.choices[0].finish_reason
235
- event.on_log(
236
- "",
237
- **{
238
- "step_suffix": step_suffix,
239
- "payload": resp.model_dump(),
240
- },
547
+
548
+ if i == 0:
549
+ _trace_first_resp(
550
+ resp,
551
+ event,
552
+ span,
553
+ start_time,
241
554
  )
242
- elif resp.usage:
243
- cumulated.append(resp)
244
555
 
245
- yield resp
246
- except Exception as e:
247
- raise e
556
+ if get_finish_reason_func is not None:
557
+ _trace_last_resp(
558
+ resp,
559
+ get_finish_reason_func,
560
+ event,
561
+ span,
562
+ )
248
563
 
564
+ if cumulated and merge_output_func is not None:
565
+ _trace_merged_resp(
566
+ cumulated,
567
+ merge_output_func,
568
+ event,
569
+ span,
570
+ )
571
+
572
+ except Exception as e:
573
+ span.set_status(
574
+ status=StatusCode.ERROR,
575
+ description=f"exception={e}",
576
+ )
577
+ event.on_log(str(e))
578
+ raise e
579
+ finally:
580
+ if not trace_context:
581
+ _parent_span_context.set(parent_ctx)
582
+
583
+ # Choose the appropriate wrapper based on function type
249
584
  if inspect.isasyncgenfunction(func):
250
- return async_iter_task
585
+ wrapper_func = async_iter_task
251
586
  elif inspect.isgeneratorfunction(func):
252
- return iter_task
587
+ wrapper_func = iter_task
253
588
  elif inspect.iscoroutinefunction(func):
254
- return async_exec
589
+ wrapper_func = async_exec
255
590
  else:
256
- return sync_exec
591
+ wrapper_func = sync_exec
592
+
593
+ # Preserve the original function's signature
594
+ try:
595
+ wrapper_func.__signature__ = inspect.signature(func)
596
+ except (ValueError, TypeError):
597
+ pass
598
+
599
+ return wrapper_func
257
600
 
258
- return task_wrapper
601
+ return wrapper
259
602
 
260
603
 
261
- def _get_start_payload(args: Any, kwargs: Any) -> Dict:
604
+ def _get_start_payload(args: Any, kwargs: Any, func: Any = None) -> Dict:
262
605
  """Extract and format the start payload from function arguments.
263
606
 
264
607
  Args:
265
608
  args (Any): Positional arguments from the function call.
266
609
  kwargs (Any): Keyword arguments from the function call.
610
+ func (Any): The function being traced (optional).
267
611
 
268
612
  Returns:
269
613
  Dict: The formatted start payload for tracing.
270
614
  """
271
- dict_args = {}
272
- if isinstance(args, tuple) and len(args) > 1:
273
- dict_args = _obj_to_dict(args[1:])
274
-
275
- dict_kwargs = _obj_to_dict(kwargs)
276
- dict_kwargs = {
277
- key: value
278
- for key, value in dict_kwargs.items()
279
- if not key.startswith("trace_")
280
- }
281
-
282
615
  merged = {}
283
- if dict_args:
616
+
617
+ # 处理位置参数:尝试将位置参数与函数签名中的参数名对应
618
+ if func is not None and isinstance(args, tuple) and len(args) > 0:
619
+ try:
620
+ sig = inspect.signature(func)
621
+ params = list(sig.parameters.values())
622
+
623
+ # 跳过self参数(如果是实例方法)
624
+ start_idx = 0
625
+ if params and params[0].name == "self":
626
+ start_idx = 1
627
+
628
+ # 将位置参数与参数名对应
629
+ for i, arg in enumerate(args[start_idx:], start=start_idx):
630
+ if i < len(params):
631
+ param = params[i]
632
+ # 只处理位置参数和位置或关键字参数,跳过*args和**kwargs
633
+ if param.kind in (
634
+ inspect.Parameter.POSITIONAL_ONLY,
635
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
636
+ ):
637
+ merged[param.name] = _obj_to_dict(arg)
638
+ except (ValueError, TypeError, IndexError):
639
+ # 如果无法获取函数签名,回退到原来的逻辑
640
+ pass
641
+
642
+ # 如果没有函数信息或无法解析,使用原来的逻辑
643
+ if not merged and isinstance(args, tuple) and len(args) > 0:
644
+ dict_args = _obj_to_dict(args)
284
645
  if isinstance(dict_args, list):
285
646
  for item in dict_args:
286
647
  if isinstance(item, dict):
@@ -288,12 +649,185 @@ def _get_start_payload(args: Any, kwargs: Any) -> Dict:
288
649
  elif isinstance(dict_args, dict):
289
650
  merged.update(dict_args)
290
651
 
652
+ # 处理关键字参数
653
+ dict_kwargs = _obj_to_dict(kwargs)
654
+ dict_kwargs = {
655
+ key: value
656
+ for key, value in dict_kwargs.items()
657
+ if not key.startswith("trace_")
658
+ }
659
+
291
660
  if dict_kwargs:
292
661
  merged.update(dict_kwargs)
293
662
 
294
663
  return merged
295
664
 
296
665
 
666
+ def _init_trace_context() -> None:
667
+ current_req_id = _current_request_id.get("")
668
+ user_req_id = TracingUtil.get_request_id()
669
+ if user_req_id and user_req_id != current_req_id:
670
+ _parent_span_context.set(None)
671
+ _current_request_id.set(user_req_id)
672
+
673
+ current_trace_header = _current_trace_header.get({})
674
+ user_trace_header = TracingUtil.get_trace_header()
675
+ if user_trace_header and user_trace_header != current_trace_header:
676
+ _current_trace_header.set(user_trace_header)
677
+ context = extract(user_trace_header)
678
+ attach(context)
679
+
680
+
681
+ def _set_request_id(parent_ctx: Any) -> None:
682
+ """Auto generate request_id for root span if not already set.
683
+
684
+ Args:
685
+ parent_ctx: Parent context. If None, this is a root span.
686
+ """
687
+ # Check if we already have a request_id (from context var or baggage)
688
+ current_request_id = TracingUtil.get_request_id()
689
+
690
+ if parent_ctx is None:
691
+ # This is a root span
692
+ if not current_request_id:
693
+ # For root spans without request_id, generate a new one
694
+ current_parent_span = _parent_span_context.get(None)
695
+ if current_parent_span is None:
696
+ # This is a truly new request, generate a new request_id
697
+ new_request_id = str(uuid.uuid4())
698
+ TracingUtil.set_request_id(new_request_id)
699
+ else:
700
+ if current_request_id and not TracingUtil.get_common_attributes().get(
701
+ "request_id",
702
+ ):
703
+ # Set common attributes from baggage request_id
704
+ TracingUtil.set_request_id(current_request_id)
705
+
706
+
707
+ def _trace_first_resp(
708
+ resp: Any,
709
+ event: EventContext,
710
+ span: Any,
711
+ start_time: int,
712
+ ) -> None:
713
+ payload = _obj_to_dict(resp)
714
+ event.on_log(
715
+ "",
716
+ **{
717
+ "step_suffix": "first_resp",
718
+ "payload": payload,
719
+ },
720
+ )
721
+ span.set_attribute(
722
+ "gen_ai.response.first_delay",
723
+ int(time.time() * 1000) - start_time,
724
+ )
725
+ _, output_value = _get_ot_type_and_value(payload)
726
+ span.set_attribute(
727
+ "gen_ai.response.first_pkg",
728
+ output_value,
729
+ )
730
+
731
+
732
+ def _trace_last_resp(
733
+ resp: Any,
734
+ func: Callable,
735
+ event: EventContext,
736
+ span: Any,
737
+ ) -> None:
738
+ resp_copy = deepcopy(resp)
739
+
740
+ finish_reason = func(resp_copy)
741
+ if finish_reason:
742
+ step_suffix = "last_resp" if finish_reason == "stop" else finish_reason
743
+ payload = _obj_to_dict(resp_copy)
744
+ event.on_log(
745
+ "",
746
+ **{
747
+ "step_suffix": step_suffix,
748
+ "payload": payload,
749
+ },
750
+ )
751
+ _, output_value = _get_ot_type_and_value(payload)
752
+ span.set_attribute(
753
+ "gen_ai.response.pkg_" + finish_reason,
754
+ output_value,
755
+ )
756
+
757
+
758
+ def _trace_merged_resp(
759
+ cumulated: Any,
760
+ func: Callable,
761
+ event: EventContext,
762
+ span: Any,
763
+ ) -> None:
764
+ cumulated_copy = deepcopy(cumulated)
765
+
766
+ merged_output = func(cumulated_copy)
767
+ end_payload = _obj_to_dict(merged_output)
768
+ output_mine_type, output_value = _get_ot_type_and_value(end_payload)
769
+ span.set_attribute(
770
+ "output.mine_type",
771
+ output_mine_type,
772
+ )
773
+ span.set_attribute(
774
+ "output.value",
775
+ output_value,
776
+ )
777
+ event.on_end(
778
+ payload=end_payload,
779
+ )
780
+
781
+
782
+ def _get_ot_type_and_value(payload: Any) -> tuple[MineType, Any]:
783
+ if isinstance(payload, dict):
784
+ mine_type = MineType.JSON
785
+ value = json.dumps(payload, ensure_ascii=False)
786
+ else:
787
+ mine_type = MineType.TEXT
788
+ if isinstance(payload, (str, int, float, bool)):
789
+ value = payload
790
+ else:
791
+ value = str(payload)
792
+ return mine_type, value
793
+
794
+
795
+ def _validate_trace_options(
796
+ trace_type: Union[TraceType, str, None] = None,
797
+ trace_name: Optional[str] = None,
798
+ is_root_span: Optional[bool] = None,
799
+ function_name: Optional[str] = None,
800
+ parent_ctx: Optional[Any] = None,
801
+ ) -> tuple[str, str | None, bool | None]:
802
+ out_is_root_span = (
803
+ is_root_span
804
+ and parent_ctx is None
805
+ and not _parent_span_context.get(None)
806
+ )
807
+
808
+ if out_is_root_span:
809
+ out_trace_type = TraceType.CHAIN
810
+ out_trace_name = "FullCodeApp"
811
+ else:
812
+ if trace_type:
813
+ if isinstance(trace_type, str):
814
+ out_trace_type = TraceType(trace_type)
815
+ else:
816
+ out_trace_type = trace_type
817
+ else:
818
+ out_trace_type = TraceType.OTHER
819
+
820
+ if trace_name:
821
+ out_trace_name = trace_name
822
+ else:
823
+ if function_name:
824
+ out_trace_name = function_name
825
+ else:
826
+ out_trace_name = str(out_trace_type).lower()
827
+
828
+ return out_trace_type, out_trace_name, out_is_root_span
829
+
830
+
297
831
  def _obj_to_dict(obj: Any) -> Any:
298
832
  """Convert an object to a dictionary representation for tracing.
299
833
 
@@ -304,10 +838,12 @@ def _obj_to_dict(obj: Any) -> Any:
304
838
  Any: The dictionary representation of the object, or the object
305
839
  itself if it's a primitive type.
306
840
  """
307
- if isinstance(obj, (str, int, float, bool, type(None))):
841
+ if obj is None:
842
+ return {}
843
+ elif isinstance(obj, (str, int, float, bool, type(None))):
308
844
  return obj
309
845
  elif isinstance(obj, dict):
310
- return {k: _obj_to_dict(v) for k, v in obj.items()} # obj
846
+ return {k: _obj_to_dict(v) for k, v in obj.items()}
311
847
  elif isinstance(obj, (list, set, tuple)):
312
848
  return [_obj_to_dict(item) for item in obj]
313
849
  elif isinstance(obj, BaseModel):
@@ -319,3 +855,92 @@ def _obj_to_dict(obj: Any) -> Any:
319
855
  except Exception as e:
320
856
  print(f"{obj} str method failed with error: {e}")
321
857
  return result
858
+
859
+
860
+ def _function_accepts_kwargs(func: Any) -> bool:
861
+ """Check if a function accepts **kwargs parameter.
862
+
863
+ Args:
864
+ func (Any): The function to check.
865
+
866
+ Returns:
867
+ bool: True if the function accepts **kwargs, False otherwise.
868
+ """
869
+ try:
870
+ sig = inspect.signature(func)
871
+ return any(
872
+ param.kind == inspect.Parameter.VAR_KEYWORD
873
+ for param in sig.parameters.values()
874
+ )
875
+ except (ValueError, TypeError):
876
+ return False
877
+
878
+
879
+ def _get_service_name() -> str:
880
+ """Get service name
881
+ Returns:
882
+ str: The extracted service name, or the original name if extraction
883
+ fails.
884
+ """
885
+
886
+ service_name = os.getenv("SERVICE_NAME") or os.getenv("DS_SVC_NAME")
887
+ if not service_name:
888
+ service_name = "agentscope_runtime-service"
889
+
890
+ pattern = r"deployment\.([^-]+(?:-[^-]+)*?)(?=-[^-]+-[^-]+$)"
891
+ match = re.search(pattern, service_name)
892
+ if match:
893
+ return match.group(1)
894
+ else:
895
+ return service_name
896
+
897
+
898
+ def _get_tracer() -> Tracer:
899
+ handlers: list[TracerHandler] = []
900
+ if _str_to_bool(os.getenv("TRACE_ENABLE_LOG", "true")):
901
+ handlers.append(LocalLogHandler(enable_console=True))
902
+
903
+ tracer = Tracer(handlers=handlers)
904
+ return tracer
905
+
906
+
907
+ # TODO: support more tracing protocols and platforms
908
+ def _get_ot_tracer() -> ot_trace.Tracer:
909
+ """Get the OpenTelemetry tracer.
910
+
911
+ Returns:
912
+ ot_trace.Tracer: The OpenTelemetry tracer instance.
913
+ """
914
+
915
+ resource = Resource(
916
+ attributes={
917
+ SERVICE_NAME: _get_service_name(),
918
+ SERVICE_VERSION: os.getenv("SERVICE_VERSION", "1.0.0"),
919
+ "source": "agentscope_runtime-source",
920
+ },
921
+ )
922
+ provider = TracerProvider(resource=resource)
923
+ if _str_to_bool(os.getenv("TRACE_ENABLE_REPORT", "false")):
924
+ span_exporter = BatchSpanProcessor(
925
+ OTLPSpanGrpcExporter(
926
+ endpoint=os.getenv("TRACE_ENDPOINT", ""),
927
+ headers=f"Authentication="
928
+ f"{os.getenv('TRACE_AUTHENTICATION', '')}",
929
+ ),
930
+ )
931
+ provider.add_span_processor(span_exporter)
932
+
933
+ if _str_to_bool(os.getenv("TRACE_ENABLE_DEBUG", "false")):
934
+ span_logger = BatchSpanProcessor(ConsoleSpanExporter())
935
+ provider.add_span_processor(span_logger)
936
+
937
+ tracer = ot_trace.get_tracer(
938
+ "agentscope_runtime",
939
+ tracer_provider=provider,
940
+ )
941
+ return tracer
942
+
943
+
944
+ _ot_tracer = _get_ot_tracer()
945
+
946
+ _tracer = _get_tracer()