veadk-python 0.2.9__py3-none-any.whl → 0.2.10__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.

Potentially problematic release.


This version of veadk-python might be problematic. Click here for more details.

Files changed (34) hide show
  1. veadk/a2a/remote_ve_agent.py +63 -6
  2. veadk/agent.py +3 -0
  3. veadk/agent_builder.py +2 -3
  4. veadk/cli/cli.py +2 -0
  5. veadk/cli/cli_kb.py +75 -0
  6. veadk/cli/cli_web.py +4 -0
  7. veadk/integrations/__init__.py +13 -0
  8. veadk/integrations/ve_viking_db_memory/__init__.py +13 -0
  9. veadk/integrations/ve_viking_db_memory/ve_viking_db_memory.py +293 -0
  10. veadk/memory/__init__.py +1 -1
  11. veadk/memory/long_term_memory.py +25 -0
  12. veadk/memory/long_term_memory_backends/mem0_backend.py +17 -2
  13. veadk/memory/long_term_memory_backends/vikingdb_memory_backend.py +4 -8
  14. veadk/memory/short_term_memory.py +9 -3
  15. veadk/memory/short_term_memory_backends/postgresql_backend.py +3 -1
  16. veadk/runner.py +23 -8
  17. veadk/tools/builtin_tools/generate_image.py +50 -16
  18. veadk/tools/builtin_tools/image_edit.py +9 -4
  19. veadk/tools/builtin_tools/image_generate.py +9 -4
  20. veadk/tools/builtin_tools/video_generate.py +6 -6
  21. veadk/tools/builtin_tools/web_search.py +10 -3
  22. veadk/tools/load_knowledgebase_tool.py +12 -0
  23. veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +5 -0
  24. veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py +7 -0
  25. veadk/tracing/telemetry/exporters/apmplus_exporter.py +82 -2
  26. veadk/tracing/telemetry/exporters/inmemory_exporter.py +8 -2
  27. veadk/tracing/telemetry/telemetry.py +41 -5
  28. veadk/version.py +1 -1
  29. {veadk_python-0.2.9.dist-info → veadk_python-0.2.10.dist-info}/METADATA +3 -3
  30. {veadk_python-0.2.9.dist-info → veadk_python-0.2.10.dist-info}/RECORD +34 -30
  31. {veadk_python-0.2.9.dist-info → veadk_python-0.2.10.dist-info}/WHEEL +0 -0
  32. {veadk_python-0.2.9.dist-info → veadk_python-0.2.10.dist-info}/entry_points.txt +0 -0
  33. {veadk_python-0.2.9.dist-info → veadk_python-0.2.10.dist-info}/licenses/LICENSE +0 -0
  34. {veadk_python-0.2.9.dist-info → veadk_python-0.2.10.dist-info}/top_level.txt +0 -0
@@ -19,6 +19,7 @@ from google.adk.sessions import (
19
19
  BaseSessionService,
20
20
  DatabaseSessionService,
21
21
  InMemorySessionService,
22
+ Session,
22
23
  )
23
24
  from pydantic import BaseModel, Field, PrivateAttr
24
25
 
@@ -106,7 +107,7 @@ class ShortTermMemory(BaseModel):
106
107
  app_name: str,
107
108
  user_id: str,
108
109
  session_id: str,
109
- ) -> None:
110
+ ) -> Session | None:
110
111
  if isinstance(self._session_service, DatabaseSessionService):
111
112
  list_sessions_response = await self._session_service.list_sessions(
112
113
  app_name=app_name, user_id=user_id
@@ -122,7 +123,12 @@ class ShortTermMemory(BaseModel):
122
123
  )
123
124
  is None
124
125
  ):
125
- # create a new session for this running
126
- await self._session_service.create_session(
126
+ return await self._session_service.create_session(
127
127
  app_name=app_name, user_id=user_id, session_id=session_id
128
128
  )
129
+ else:
130
+ logger.info(
131
+ f"Session {session_id} already exists with app_name={app_name} user_id={user_id}."
132
+ )
133
+
134
+ return None
@@ -14,6 +14,7 @@
14
14
 
15
15
  from functools import cached_property
16
16
  from typing import Any
17
+ from venv import logger
17
18
 
18
19
  from google.adk.sessions import (
19
20
  BaseSessionService,
@@ -33,7 +34,8 @@ class PostgreSqlSTMBackend(BaseShortTermMemoryBackend):
33
34
  postgresql_config: PostgreSqlConfig = Field(default_factory=PostgreSqlConfig)
34
35
 
35
36
  def model_post_init(self, context: Any) -> None:
36
- self._db_url = f"postgresql+psycopg2://{self.postgresql_config.user}:{self.postgresql_config.password}@{self.postgresql_config.host}:{self.postgresql_config.port}/{self.postgresql_config.database}"
37
+ self._db_url = f"postgresql://{self.postgresql_config.user}:{self.postgresql_config.password}@{self.postgresql_config.host}:{self.postgresql_config.port}/{self.postgresql_config.database}"
38
+ logger.debug(self._db_url)
37
39
 
38
40
  @cached_property
39
41
  @override
veadk/runner.py CHANGED
@@ -12,8 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- import os
16
15
  import functools
16
+ import os
17
17
  from types import MethodType
18
18
  from typing import Union
19
19
 
@@ -249,15 +249,21 @@ class Runner(ADKRunner):
249
249
  )
250
250
 
251
251
  if self.short_term_memory:
252
- await self.short_term_memory.create_session(
253
- app_name=self.app_name, user_id=self.user_id, session_id=session_id
252
+ session = await self.short_term_memory.create_session(
253
+ app_name=self.app_name, user_id=user_id, session_id=session_id
254
+ )
255
+ assert session, (
256
+ f"Failed to create session with app_name={self.app_name}, user_id={user_id}, session_id={session_id}, "
257
+ )
258
+ logger.debug(
259
+ f"Auto create session: {session.id}, user_id: {session.user_id}, app_name: {self.app_name}"
254
260
  )
255
261
 
256
262
  final_output = ""
257
263
  for converted_message in converted_messages:
258
264
  try:
259
265
  async for event in self.run_async(
260
- user_id=self.user_id,
266
+ user_id=user_id,
261
267
  session_id=session_id,
262
268
  new_message=converted_message,
263
269
  run_config=run_config,
@@ -359,19 +365,28 @@ class Runner(ADKRunner):
359
365
  )
360
366
  return eval_set_path
361
367
 
362
- async def save_session_to_long_term_memory(self, session_id: str) -> None:
368
+ async def save_session_to_long_term_memory(
369
+ self, session_id: str, user_id: str = "", app_name: str = ""
370
+ ) -> None:
363
371
  if not self.long_term_memory:
364
372
  logger.warning("Long-term memory is not enabled. Failed to save session.")
365
373
  return
366
374
 
375
+ if not user_id:
376
+ user_id = self.user_id
377
+
378
+ if not app_name:
379
+ app_name = self.app_name
380
+
367
381
  session = await self.session_service.get_session(
368
- app_name=self.app_name,
369
- user_id=self.user_id,
382
+ app_name=app_name,
383
+ user_id=user_id,
370
384
  session_id=session_id,
371
385
  )
386
+
372
387
  if not session:
373
388
  logger.error(
374
- f"Session {session_id} not found in session service, cannot save to long-term memory."
389
+ f"Session {session_id} (app_name={app_name}, user_id={user_id}) not found in session service, cannot save to long-term memory."
375
390
  )
376
391
  return
377
392
 
@@ -12,28 +12,30 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import base64
16
+ import json
17
+ import mimetypes
18
+ import traceback
15
19
  from typing import Dict
16
20
 
17
21
  from google.adk.tools import ToolContext
18
- from veadk.config import getenv
19
- from veadk.consts import DEFAULT_IMAGE_GENERATE_MODEL_NAME, DEFAULT_MODEL_AGENT_API_BASE
20
-
21
- import base64
22
- from volcenginesdkarkruntime import Ark
22
+ from google.genai.types import Blob, Part
23
23
  from opentelemetry import trace
24
- import traceback
25
- from veadk.version import VERSION
26
24
  from opentelemetry.trace import Span
27
- from veadk.utils.logger import get_logger
25
+ from volcenginesdkarkruntime import Ark
28
26
  from volcenginesdkarkruntime.types.images.images import SequentialImageGenerationOptions
29
- import json
30
27
 
28
+ from veadk.config import getenv
29
+ from veadk.consts import DEFAULT_IMAGE_GENERATE_MODEL_NAME, DEFAULT_MODEL_AGENT_API_BASE
30
+ from veadk.utils.logger import get_logger
31
+ from veadk.utils.misc import formatted_timestamp, read_png_to_bytes
32
+ from veadk.version import VERSION
31
33
 
32
34
  logger = get_logger(__name__)
33
35
 
34
36
  client = Ark(
35
37
  api_key=getenv("MODEL_AGENT_API_KEY"),
36
- base_url=DEFAULT_MODEL_AGENT_API_BASE,
38
+ base_url=getenv("MODEL_AGENT_API_BASE", DEFAULT_MODEL_AGENT_API_BASE),
37
39
  )
38
40
 
39
41
 
@@ -121,7 +123,7 @@ async def image_generate(
121
123
  - size 推荐使用 2048x2048 或表格里的标准比例,确保生成质量。
122
124
  """
123
125
 
124
- success_list = []
126
+ success_list: list[dict] = []
125
127
  error_list = []
126
128
 
127
129
  for idx, item in enumerate(tasks):
@@ -184,7 +186,9 @@ async def image_generate(
184
186
  and max_images
185
187
  ):
186
188
  response = client.images.generate(
187
- model=DEFAULT_IMAGE_GENERATE_MODEL_NAME,
189
+ model=getenv(
190
+ "MODEL_IMAGE_NAME", DEFAULT_IMAGE_GENERATE_MODEL_NAME
191
+ ),
188
192
  **inputs,
189
193
  sequential_image_generation_options=SequentialImageGenerationOptions(
190
194
  max_images=max_images
@@ -192,7 +196,10 @@ async def image_generate(
192
196
  )
193
197
  else:
194
198
  response = client.images.generate(
195
- model=DEFAULT_IMAGE_GENERATE_MODEL_NAME, **inputs
199
+ model=getenv(
200
+ "MODEL_IMAGE_NAME", DEFAULT_IMAGE_GENERATE_MODEL_NAME
201
+ ),
202
+ **inputs,
196
203
  )
197
204
  if not response.error:
198
205
  for i, image_data in enumerate(response.data):
@@ -261,8 +268,12 @@ async def image_generate(
261
268
  output_part=output_part,
262
269
  output_tokens=output_tokens,
263
270
  total_tokens=total_tokens,
264
- request_model=DEFAULT_IMAGE_GENERATE_MODEL_NAME,
265
- response_model=DEFAULT_IMAGE_GENERATE_MODEL_NAME,
271
+ request_model=getenv(
272
+ "MODEL_IMAGE_NAME", DEFAULT_IMAGE_GENERATE_MODEL_NAME
273
+ ),
274
+ response_model=getenv(
275
+ "MODEL_IMAGE_NAME", DEFAULT_IMAGE_GENERATE_MODEL_NAME
276
+ ),
266
277
  )
267
278
  if len(success_list) == 0:
268
279
  return {
@@ -271,6 +282,28 @@ async def image_generate(
271
282
  "error_list": error_list,
272
283
  }
273
284
  else:
285
+ app_name = tool_context._invocation_context.app_name
286
+ user_id = tool_context._invocation_context.user_id
287
+ session_id = tool_context._invocation_context.session.id
288
+
289
+ artifact_service = tool_context._invocation_context.artifact_service
290
+ if artifact_service:
291
+ for image in success_list:
292
+ for _, image_tos_url in image.items():
293
+ filename = f"artifact_{formatted_timestamp()}"
294
+ await artifact_service.save_artifact(
295
+ app_name=app_name,
296
+ user_id=user_id,
297
+ session_id=session_id,
298
+ filename=filename,
299
+ artifact=Part(
300
+ inline_data=Blob(
301
+ display_name=filename,
302
+ data=read_png_to_bytes(image_tos_url),
303
+ mime_type=mimetypes.guess_type(image_tos_url)[0],
304
+ )
305
+ ),
306
+ )
274
307
  return {
275
308
  "status": "success",
276
309
  "success_list": success_list,
@@ -332,10 +365,11 @@ def add_span_attributes(
332
365
 
333
366
  def _upload_image_to_tos(image_bytes: bytes, object_key: str) -> None:
334
367
  try:
335
- from veadk.integrations.ve_tos.ve_tos import VeTOS
336
368
  import os
337
369
  from datetime import datetime
338
370
 
371
+ from veadk.integrations.ve_tos.ve_tos import VeTOS
372
+
339
373
  timestamp: str = datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3]
340
374
  object_key = f"{timestamp}-{object_key}"
341
375
  bucket_name = os.getenv("DATABASE_TOS_BUCKET")
@@ -29,7 +29,7 @@ logger = get_logger(__name__)
29
29
 
30
30
  client = Ark(
31
31
  api_key=getenv("MODEL_AGENT_API_KEY"),
32
- base_url=DEFAULT_MODEL_AGENT_API_BASE,
32
+ base_url=getenv("MODEL_AGENT_API_BASE", DEFAULT_MODEL_AGENT_API_BASE),
33
33
  )
34
34
 
35
35
 
@@ -123,7 +123,8 @@ async def image_edit(
123
123
  "parts.1.image_url.url": origin_image,
124
124
  }
125
125
  response = client.images.generate(
126
- model=DEFAULT_IMAGE_EDIT_MODEL_NAME, **inputs
126
+ model=getenv("MODEL_EDIT_NAME", DEFAULT_IMAGE_EDIT_MODEL_NAME),
127
+ **inputs,
127
128
  )
128
129
  output_part = None
129
130
  if response.data and len(response.data) > 0:
@@ -175,8 +176,12 @@ async def image_edit(
175
176
  output_part=output_part,
176
177
  output_tokens=response.usage.output_tokens,
177
178
  total_tokens=response.usage.total_tokens,
178
- request_model=DEFAULT_IMAGE_EDIT_MODEL_NAME,
179
- response_model=DEFAULT_IMAGE_EDIT_MODEL_NAME,
179
+ request_model=getenv(
180
+ "MODEL_EDIT_NAME", DEFAULT_IMAGE_EDIT_MODEL_NAME
181
+ ),
182
+ response_model=getenv(
183
+ "MODEL_EDIT_NAME", DEFAULT_IMAGE_EDIT_MODEL_NAME
184
+ ),
180
185
  )
181
186
 
182
187
  except Exception as e:
@@ -30,7 +30,7 @@ logger = get_logger(__name__)
30
30
 
31
31
  client = Ark(
32
32
  api_key=getenv("MODEL_AGENT_API_KEY"),
33
- base_url=DEFAULT_MODEL_AGENT_API_BASE,
33
+ base_url=getenv("MODEL_AGENT_API_BASE", DEFAULT_MODEL_AGENT_API_BASE),
34
34
  )
35
35
 
36
36
 
@@ -120,7 +120,8 @@ async def image_generate(
120
120
  "content": json.dumps(inputs, ensure_ascii=False),
121
121
  }
122
122
  response = client.images.generate(
123
- model=DEFAULT_TEXT_TO_IMAGE_MODEL_NAME, **inputs
123
+ model=getenv("MODEL_IMAGE_NAME", DEFAULT_TEXT_TO_IMAGE_MODEL_NAME),
124
+ **inputs,
124
125
  )
125
126
  output_part = None
126
127
  if response.data and len(response.data) > 0:
@@ -172,8 +173,12 @@ async def image_generate(
172
173
  output_part=output_part,
173
174
  output_tokens=response.usage.output_tokens,
174
175
  total_tokens=response.usage.total_tokens,
175
- request_model=DEFAULT_TEXT_TO_IMAGE_MODEL_NAME,
176
- response_model=DEFAULT_TEXT_TO_IMAGE_MODEL_NAME,
176
+ request_model=getenv(
177
+ "MODEL_IMAGE_NAME", DEFAULT_TEXT_TO_IMAGE_MODEL_NAME
178
+ ),
179
+ response_model=getenv(
180
+ "MODEL_IMAGE_NAME", DEFAULT_TEXT_TO_IMAGE_MODEL_NAME
181
+ ),
177
182
  )
178
183
 
179
184
  except Exception as e:
@@ -34,7 +34,7 @@ logger = get_logger(__name__)
34
34
 
35
35
  client = Ark(
36
36
  api_key=getenv("MODEL_AGENT_API_KEY"),
37
- base_url=DEFAULT_MODEL_AGENT_API_BASE,
37
+ base_url=getenv("MODEL_AGENT_API_BASE", DEFAULT_MODEL_AGENT_API_BASE),
38
38
  )
39
39
 
40
40
 
@@ -43,7 +43,7 @@ async def generate(prompt, first_frame_image=None, last_frame_image=None):
43
43
  if first_frame_image is None:
44
44
  logger.debug("text generation")
45
45
  response = client.content_generation.tasks.create(
46
- model=DEFAULT_VIDEO_MODEL_NAME,
46
+ model=getenv("MODEL_VIDEO_NAME", DEFAULT_VIDEO_MODEL_NAME),
47
47
  content=[
48
48
  {"type": "text", "text": prompt},
49
49
  ],
@@ -51,7 +51,7 @@ async def generate(prompt, first_frame_image=None, last_frame_image=None):
51
51
  elif last_frame_image is None:
52
52
  logger.debug("first frame generation")
53
53
  response = client.content_generation.tasks.create(
54
- model=DEFAULT_VIDEO_MODEL_NAME,
54
+ model=getenv("MODEL_VIDEO_NAME", DEFAULT_VIDEO_MODEL_NAME),
55
55
  content=cast(
56
56
  list[CreateTaskContentParam], # avoid IDE warning
57
57
  [
@@ -66,7 +66,7 @@ async def generate(prompt, first_frame_image=None, last_frame_image=None):
66
66
  else:
67
67
  logger.debug("last frame generation")
68
68
  response = client.content_generation.tasks.create(
69
- model=DEFAULT_VIDEO_MODEL_NAME,
69
+ model=getenv("MODEL_VIDEO_NAME", DEFAULT_VIDEO_MODEL_NAME),
70
70
  content=[
71
71
  {"type": "text", "text": prompt},
72
72
  {
@@ -263,8 +263,8 @@ async def video_generate(params: list, tool_context: ToolContext) -> Dict:
263
263
  output_part=output_part,
264
264
  output_tokens=total_tokens,
265
265
  total_tokens=total_tokens,
266
- request_model=DEFAULT_VIDEO_MODEL_NAME,
267
- response_model=DEFAULT_VIDEO_MODEL_NAME,
266
+ request_model=getenv("MODEL_VIDEO_NAME", DEFAULT_VIDEO_MODEL_NAME),
267
+ response_model=getenv("MODEL_VIDEO_NAME", DEFAULT_VIDEO_MODEL_NAME),
268
268
  )
269
269
 
270
270
  if len(success_list) == 0:
@@ -23,6 +23,7 @@ import json
23
23
  from urllib.parse import quote
24
24
 
25
25
  import requests
26
+ from google.adk.tools import ToolContext
26
27
 
27
28
  from veadk.config import getenv
28
29
  from veadk.utils.logger import get_logger
@@ -151,7 +152,7 @@ def request(method, date, query, header, ak, sk, action, body):
151
152
  return r.json()
152
153
 
153
154
 
154
- def web_search(query: str) -> list[str]:
155
+ def web_search(query: str, tool_context: ToolContext) -> list[str]:
155
156
  """Search a query in websites.
156
157
 
157
158
  Args:
@@ -166,8 +167,14 @@ def web_search(query: str) -> list[str]:
166
167
  "Count": 5,
167
168
  "NeedSummary": True,
168
169
  }
169
- ak = getenv("VOLCENGINE_ACCESS_KEY")
170
- sk = getenv("VOLCENGINE_SECRET_KEY")
170
+
171
+ ak = tool_context.state.get("VOLCENGINE_ACCESS_KEY")
172
+ if not ak:
173
+ ak = getenv("VOLCENGINE_ACCESS_KEY")
174
+
175
+ sk = tool_context.state.get("VOLCENGINE_SECRET_KEY")
176
+ if not sk:
177
+ sk = getenv("VOLCENGINE_SECRET_KEY")
171
178
 
172
179
  now = datetime.datetime.utcnow()
173
180
  response_body = request(
@@ -25,6 +25,9 @@ from typing_extensions import override
25
25
 
26
26
  from veadk.knowledgebase import KnowledgeBase
27
27
  from veadk.knowledgebase.entry import KnowledgebaseEntry
28
+ from veadk.utils.logger import get_logger
29
+
30
+ logger = get_logger(__name__)
28
31
 
29
32
  if TYPE_CHECKING:
30
33
  from google.adk.models.llm_request import LlmRequest
@@ -96,6 +99,15 @@ class LoadKnowledgebaseTool(FunctionTool):
96
99
 
97
100
  def __init__(self):
98
101
  super().__init__(load_knowledgebase)
102
+ global knowledgebase
103
+ if knowledgebase is None:
104
+ logger.info(
105
+ "Get global knowledgebase instance failed, failed to set knowledgebase tool backend."
106
+ )
107
+ else:
108
+ if not self.custom_metadata:
109
+ self.custom_metadata = {}
110
+ self.custom_metadata["backend"] = knowledgebase.backend
99
111
 
100
112
  @override
101
113
  def _get_declaration(self) -> types.FunctionDeclaration | None:
@@ -325,6 +325,10 @@ def llm_gen_ai_operation_name(params: LLMAttributesParams) -> ExtractorResponse:
325
325
  return ExtractorResponse(content="chat")
326
326
 
327
327
 
328
+ def llm_gen_ai_span_kind(params: LLMAttributesParams) -> ExtractorResponse:
329
+ return ExtractorResponse(content="llm")
330
+
331
+
328
332
  # def llm_gen_ai_system_message(params: LLMAttributesParams) -> ExtractorResponse:
329
333
  # event_attributes = {
330
334
  # "content": str(params.llm_request.config.system_instruction),
@@ -559,6 +563,7 @@ LLM_ATTRIBUTES = {
559
563
  "gen_ai.is_streaming": llm_gen_ai_is_streaming,
560
564
  # -> 1.4. span kind
561
565
  "gen_ai.operation.name": llm_gen_ai_operation_name,
566
+ "gen_ai.span.kind": llm_gen_ai_span_kind, # apmplus required
562
567
  # -> 1.5. inputs
563
568
  "gen_ai.prompt": llm_gen_ai_prompt,
564
569
  # -> 1.6. outputs
@@ -23,6 +23,10 @@ def tool_gen_ai_operation_name(params: ToolAttributesParams) -> ExtractorRespons
23
23
  return ExtractorResponse(content="execute_tool")
24
24
 
25
25
 
26
+ def tool_gen_ai_span_kind(params: ToolAttributesParams) -> ExtractorResponse:
27
+ return ExtractorResponse(content="tool")
28
+
29
+
26
30
  def tool_gen_ai_tool_message(params: ToolAttributesParams) -> ExtractorResponse:
27
31
  tool_input = {
28
32
  "role": "tool",
@@ -73,4 +77,7 @@ TOOL_ATTRIBUTES = {
73
77
  "gen_ai.tool.output": tool_gen_ai_tool_output, # TLS required
74
78
  "cozeloop.input": tool_gen_ai_tool_input, # CozeLoop required
75
79
  "cozeloop.output": tool_gen_ai_tool_output, # CozeLoop required
80
+ "gen_ai.span.kind": tool_gen_ai_span_kind, # apmplus required
81
+ "gen_ai.input": tool_gen_ai_tool_input, # apmplus required
82
+ "gen_ai.output": tool_gen_ai_tool_output, # apmplus required
76
83
  }
@@ -17,8 +17,10 @@ from dataclasses import dataclass
17
17
  from typing import Any
18
18
 
19
19
  from google.adk.agents.invocation_context import InvocationContext
20
+ from google.adk.events import Event
20
21
  from google.adk.models.llm_request import LlmRequest
21
22
  from google.adk.models.llm_response import LlmResponse
23
+ from google.adk.tools import BaseTool
22
24
  from opentelemetry import metrics, trace
23
25
  from opentelemetry import metrics as metrics_api
24
26
  from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
@@ -124,6 +126,12 @@ class Meters:
124
126
  "gen_ai.chat_completions.streaming_time_per_output_token"
125
127
  )
126
128
 
129
+ # apmplus metrics
130
+ # span duration
131
+ APMPLUS_SPAN_LATENCY = "apmplus_span_latency"
132
+ # tool token usage
133
+ APMPLUS_TOOL_TOKEN_USAGE = "apmplus_tool_token_usage"
134
+
127
135
 
128
136
  class MeterUploader:
129
137
  def __init__(
@@ -195,7 +203,21 @@ class MeterUploader:
195
203
  explicit_bucket_boundaries_advisory=_GEN_AI_SERVER_TIME_PER_OUTPUT_TOKEN_BUCKETS,
196
204
  )
197
205
 
198
- def record(
206
+ # apmplus metrics for veadk dashboard
207
+ self.apmplus_span_latency = self.meter.create_histogram(
208
+ name=Meters.APMPLUS_SPAN_LATENCY,
209
+ description="Latency of span",
210
+ unit="s",
211
+ explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS,
212
+ )
213
+ self.apmplus_tool_token_usage = self.meter.create_histogram(
214
+ name=Meters.APMPLUS_TOOL_TOKEN_USAGE,
215
+ description="Token consumption of APMPlus tool token",
216
+ unit="count",
217
+ explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS,
218
+ )
219
+
220
+ def record_call_llm(
199
221
  self,
200
222
  invocation_context: InvocationContext,
201
223
  event_id: str,
@@ -205,7 +227,8 @@ class MeterUploader:
205
227
  attributes = {
206
228
  "gen_ai_system": "volcengine",
207
229
  "gen_ai_response_model": llm_request.model,
208
- "gen_ai_operation_name": "chat_completions",
230
+ "gen_ai_operation_name": "chat",
231
+ "gen_ai_operation_type": "llm",
209
232
  "stream": "false",
210
233
  "server_address": "api.volcengine.com",
211
234
  } # required by Volcengine APMPlus
@@ -267,6 +290,63 @@ class MeterUploader:
267
290
  # time_per_output_token, attributes=attributes
268
291
  # )
269
292
 
293
+ # add span name attribute
294
+ span = trace.get_current_span()
295
+ if not span:
296
+ return
297
+
298
+ # record span latency
299
+ if hasattr(span, "start_time") and self.apmplus_span_latency:
300
+ # span 耗时
301
+ duration = (time.time_ns() - span.start_time) / 1e9 # type: ignore
302
+ self.apmplus_span_latency.record(duration, attributes=attributes)
303
+
304
+ def record_tool_call(
305
+ self,
306
+ tool: BaseTool,
307
+ args: dict[str, Any],
308
+ function_response_event: Event,
309
+ ):
310
+ logger.debug(f"Record tool call work in progress. Tool: {tool.name}")
311
+ span = trace.get_current_span()
312
+ if not span:
313
+ return
314
+ operation_type = "tool"
315
+ operation_name = tool.name
316
+ operation_backend = ""
317
+ if tool.custom_metadata:
318
+ operation_backend = tool.custom_metadata.get("backend", "")
319
+
320
+ attributes = {
321
+ "gen_ai_operation_name": operation_name,
322
+ "gen_ai_operation_type": operation_type,
323
+ "gen_ai_operation_backend": operation_backend,
324
+ }
325
+
326
+ if hasattr(span, "start_time") and self.apmplus_span_latency:
327
+ # span 耗时
328
+ duration = (time.time_ns() - span.start_time) / 1e9 # type: ignore
329
+ self.apmplus_span_latency.record(duration, attributes=attributes)
330
+
331
+ if self.apmplus_tool_token_usage and hasattr(span, "attributes"):
332
+ tool_input = span.attributes["gen_ai.tool.input"]
333
+ tool_token_usage_input = (
334
+ len(tool_input) / 4
335
+ ) # tool token 数量,使用文本长度/4
336
+ input_tool_token_attributes = {**attributes, "token_type": "input"}
337
+ self.apmplus_tool_token_usage.record(
338
+ tool_token_usage_input, attributes=input_tool_token_attributes
339
+ )
340
+
341
+ tool_output = span.attributes["gen_ai.tool.output"]
342
+ tool_token_usage_output = (
343
+ len(tool_output) / 4
344
+ ) # tool token 数量,使用文本长度/4
345
+ output_tool_token_attributes = {**attributes, "token_type": "output"}
346
+ self.apmplus_tool_token_usage.record(
347
+ tool_token_usage_output, attributes=output_tool_token_attributes
348
+ )
349
+
270
350
 
271
351
  class APMPlusExporterConfig(BaseModel):
272
352
  endpoint: str = Field(
@@ -79,14 +79,20 @@ class _InMemorySpanProcessor(export.SimpleSpanProcessor):
79
79
  def on_start(self, span, parent_context) -> None:
80
80
  if span.name.startswith("invocation"):
81
81
  span.set_attribute("gen_ai.operation.name", "chain")
82
+ span.set_attribute("gen_ai.span.kind", "workflow")
82
83
  span.set_attribute("gen_ai.usage.total_tokens", 0)
83
-
84
84
  ctx = set_value("invocation_span_instance", span, context=parent_context)
85
+ # suppress instrumentation for llm to avoid auto instrument from apmplus, such as openai
86
+ ctx = set_value(
87
+ "suppress_language_model_instrumentation", True, context=ctx
88
+ )
89
+
85
90
  token = attach(ctx) # mount context on `invocation` root span in Google ADK
86
91
  setattr(span, "_invocation_token", token) # for later detach
87
92
 
88
- if span.name.startswith("agent_run"):
93
+ if span.name.startswith("agent_run") or span.name.startswith("invoke_agent"):
89
94
  span.set_attribute("gen_ai.operation.name", "agent")
95
+ span.set_attribute("gen_ai.span.kind", "agent")
90
96
 
91
97
  ctx = set_value("agent_run_span_instance", span, context=parent_context)
92
98
  token = attach(ctx)