bedrock-agentcore-starter-toolkit 0.1.10__py3-none-any.whl → 0.1.12__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 bedrock-agentcore-starter-toolkit might be problematic. Click here for more details.

Files changed (19) hide show
  1. bedrock_agentcore_starter_toolkit/cli/cli.py +1 -1
  2. bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +65 -46
  3. bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +15 -1
  4. bedrock_agentcore_starter_toolkit/operations/gateway/client.py +139 -0
  5. bedrock_agentcore_starter_toolkit/operations/runtime/destroy.py +61 -56
  6. bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +1 -1
  7. bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +15 -0
  8. bedrock_agentcore_starter_toolkit/services/import_agent/scripts/base_bedrock_translate.py +4 -3
  9. bedrock_agentcore_starter_toolkit/services/import_agent/utils.py +14 -0
  10. bedrock_agentcore_starter_toolkit/services/runtime.py +5 -1
  11. bedrock_agentcore_starter_toolkit/services/xray.py +161 -0
  12. bedrock_agentcore_starter_toolkit/utils/runtime/logs.py +12 -0
  13. bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +11 -25
  14. {bedrock_agentcore_starter_toolkit-0.1.10.dist-info → bedrock_agentcore_starter_toolkit-0.1.12.dist-info}/METADATA +18 -2
  15. {bedrock_agentcore_starter_toolkit-0.1.10.dist-info → bedrock_agentcore_starter_toolkit-0.1.12.dist-info}/RECORD +19 -18
  16. {bedrock_agentcore_starter_toolkit-0.1.10.dist-info → bedrock_agentcore_starter_toolkit-0.1.12.dist-info}/WHEEL +0 -0
  17. {bedrock_agentcore_starter_toolkit-0.1.10.dist-info → bedrock_agentcore_starter_toolkit-0.1.12.dist-info}/entry_points.txt +0 -0
  18. {bedrock_agentcore_starter_toolkit-0.1.10.dist-info → bedrock_agentcore_starter_toolkit-0.1.12.dist-info}/licenses/LICENSE.txt +0 -0
  19. {bedrock_agentcore_starter_toolkit-0.1.10.dist-info → bedrock_agentcore_starter_toolkit-0.1.12.dist-info}/licenses/NOTICE.txt +0 -0
@@ -39,14 +39,18 @@ def destroy_bedrock_agentcore(
39
39
  ValueError: If agent is not found or not deployed
40
40
  RuntimeError: If destruction fails
41
41
  """
42
- log.info("Starting destroy operation for agent: %s (dry_run=%s, delete_ecr_repo=%s)",
43
- agent_name or "default", dry_run, delete_ecr_repo)
42
+ log.info(
43
+ "Starting destroy operation for agent: %s (dry_run=%s, delete_ecr_repo=%s)",
44
+ agent_name or "default",
45
+ dry_run,
46
+ delete_ecr_repo,
47
+ )
44
48
 
45
49
  try:
46
50
  # Load configuration
47
51
  project_config = load_config(config_path)
48
52
  agent_config = project_config.get_agent_config(agent_name)
49
-
53
+
50
54
  if not agent_config:
51
55
  raise ValueError(f"Agent '{agent_name or 'default'}' not found in configuration")
52
56
 
@@ -60,32 +64,36 @@ def destroy_bedrock_agentcore(
60
64
 
61
65
  # Initialize AWS session and clients
62
66
  session = boto3.Session(region_name=agent_config.aws.region)
63
-
67
+
64
68
  # 1. Destroy Bedrock AgentCore endpoint (if exists)
65
69
  _destroy_agentcore_endpoint(session, agent_config, result, dry_run)
66
-
70
+
67
71
  # 2. Destroy Bedrock AgentCore agent
68
72
  _destroy_agentcore_agent(session, agent_config, result, dry_run)
69
-
73
+
70
74
  # 3. Remove ECR images and optionally the repository
71
75
  _destroy_ecr_images(session, agent_config, result, dry_run, delete_ecr_repo)
72
-
76
+
73
77
  # 4. Remove CodeBuild project
74
78
  _destroy_codebuild_project(session, agent_config, result, dry_run)
75
79
 
76
80
  # 5. Remove CodeBuild IAM Role
77
81
  _destroy_codebuild_iam_role(session, agent_config, result, dry_run)
78
-
82
+
79
83
  # 6. Remove IAM execution role (if not used by other agents)
80
84
  _destroy_iam_role(session, project_config, agent_config, result, dry_run)
81
-
85
+
82
86
  # 7. Clean up configuration
83
87
  if not dry_run and not result.errors:
84
88
  _cleanup_agent_config(config_path, project_config, agent_config.name, result)
85
89
 
86
- log.info("Destroy operation completed. Resources removed: %d, Warnings: %d, Errors: %d",
87
- len(result.resources_removed), len(result.warnings), len(result.errors))
88
-
90
+ log.info(
91
+ "Destroy operation completed. Resources removed: %d, Warnings: %d, Errors: %d",
92
+ len(result.resources_removed),
93
+ len(result.warnings),
94
+ len(result.errors),
95
+ )
96
+
89
97
  return result
90
98
 
91
99
  except Exception as e:
@@ -105,7 +113,7 @@ def _destroy_agentcore_endpoint(
105
113
 
106
114
  try:
107
115
  client = BedrockAgentCoreClient(agent_config.aws.region)
108
-
116
+
109
117
  agent_id = agent_config.bedrock_agentcore.agent_id
110
118
  if not agent_id:
111
119
  result.warnings.append("No agent ID found, skipping endpoint destruction")
@@ -116,12 +124,10 @@ def _destroy_agentcore_endpoint(
116
124
  endpoint_response = client.get_agent_runtime_endpoint(agent_id)
117
125
  endpoint_name = endpoint_response.get("name", "DEFAULT")
118
126
  endpoint_arn = endpoint_response.get("agentRuntimeEndpointArn")
119
-
127
+
120
128
  # Special case: DEFAULT endpoint cannot be explicitly deleted
121
129
  if endpoint_name == "DEFAULT":
122
- result.warnings.append(
123
- "DEFAULT endpoint cannot be explicitly deleted, skipping"
124
- )
130
+ result.warnings.append("DEFAULT endpoint cannot be explicitly deleted, skipping")
125
131
  log.info("Skipping deletion of DEFAULT endpoint")
126
132
  return
127
133
 
@@ -143,7 +149,7 @@ def _destroy_agentcore_endpoint(
143
149
  result.warnings.append("Endpoint not found or already deleted during deletion")
144
150
  else:
145
151
  result.warnings.append("No endpoint ARN found for agent")
146
-
152
+
147
153
  except ClientError as e:
148
154
  if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
149
155
  result.warnings.append(f"Failed to get endpoint info: {e}")
@@ -168,7 +174,8 @@ def _destroy_agentcore_agent(
168
174
  return
169
175
 
170
176
  try:
171
- client = BedrockAgentCoreClient(agent_config.aws.region)
177
+ # Initialize client to enable exception handling path for tests
178
+ BedrockAgentCoreClient(agent_config.aws.region)
172
179
  agent_arn = agent_config.bedrock_agentcore.agent_arn
173
180
  agent_id = agent_config.bedrock_agentcore.agent_id
174
181
 
@@ -212,18 +219,18 @@ def _destroy_ecr_images(
212
219
  # Create ECR client with explicit region specification
213
220
  ecr_client = session.client("ecr", region_name=agent_config.aws.region)
214
221
  ecr_uri = agent_config.aws.ecr_repository
215
-
222
+
216
223
  # Extract repository name from URI
217
224
  # Format: account.dkr.ecr.region.amazonaws.com/repo-name
218
225
  repo_name = ecr_uri.split("/")[-1]
219
-
226
+
220
227
  log.info("Checking ECR repository: %s in region: %s", repo_name, agent_config.aws.region)
221
228
 
222
229
  try:
223
230
  # List all images in the repository (both tagged and untagged)
224
231
  response = ecr_client.list_images(repositoryName=repo_name)
225
232
  log.debug("ECR list_images response: %s", response)
226
-
233
+
227
234
  # Fix: use correct response key 'imageIds' instead of 'imageDetails'
228
235
  all_images = response.get("imageIds", [])
229
236
  if not all_images:
@@ -238,7 +245,7 @@ def _destroy_ecr_images(
238
245
  return
239
246
 
240
247
  if dry_run:
241
- # Fix: imageIds structure has imageTag (string) not imageTags (array)
248
+ # Fix: imageIds structure has imageTag (string) not imageTags (array)
242
249
  tagged_count = len([img for img in all_images if img.get("imageTag")])
243
250
  untagged_count = len([img for img in all_images if not img.get("imageTag")])
244
251
  result.resources_removed.append(
@@ -250,18 +257,18 @@ def _destroy_ecr_images(
250
257
 
251
258
  # Prepare images for deletion - imageIds are already in the correct format
252
259
  images_to_delete = []
253
-
260
+
254
261
  for image in all_images:
255
262
  # imageIds structure already contains the correct identifiers
256
263
  image_id = {}
257
-
264
+
258
265
  # If image has a tag, use it
259
266
  if image.get("imageTag"):
260
267
  image_id["imageTag"] = image["imageTag"]
261
- # If no tag, use image digest
268
+ # If no tag, use image digest
262
269
  elif image.get("imageDigest"):
263
270
  image_id["imageDigest"] = image["imageDigest"]
264
-
271
+
265
272
  if image_id:
266
273
  images_to_delete.append(image_id)
267
274
 
@@ -269,34 +276,32 @@ def _destroy_ecr_images(
269
276
  # Delete images in batches (ECR has a limit of 100 images per batch)
270
277
  batch_size = 100
271
278
  total_deleted = 0
272
-
279
+
273
280
  for i in range(0, len(images_to_delete), batch_size):
274
- batch = images_to_delete[i:i + batch_size]
275
-
276
- delete_response = ecr_client.batch_delete_image(
277
- repositoryName=repo_name,
278
- imageIds=batch
279
- )
280
-
281
+ batch = images_to_delete[i : i + batch_size]
282
+
283
+ delete_response = ecr_client.batch_delete_image(repositoryName=repo_name, imageIds=batch)
284
+
281
285
  deleted_images = delete_response.get("imageIds", [])
282
286
  total_deleted += len(deleted_images)
283
-
287
+
284
288
  # Log any failures in this batch
285
289
  failures = delete_response.get("failures", [])
286
290
  for failure in failures:
287
- log.warning("Failed to delete image: %s - %s",
288
- failure.get("imageId"), failure.get("failureReason"))
291
+ log.warning(
292
+ "Failed to delete image: %s - %s", failure.get("imageId"), failure.get("failureReason")
293
+ )
289
294
 
290
295
  result.resources_removed.append(f"ECR images: {total_deleted} images from {repo_name}")
291
296
  log.info("Deleted %d ECR images from %s", total_deleted, repo_name)
292
-
297
+
293
298
  # Log any partial failures
294
299
  if total_deleted < len(images_to_delete):
295
300
  failed_count = len(images_to_delete) - total_deleted
296
301
  result.warnings.append(
297
302
  f"Some ECR images could not be deleted: {failed_count} out of {len(images_to_delete)} failed"
298
303
  )
299
-
304
+
300
305
  # Delete the repository if requested and all images were deleted successfully
301
306
  if delete_ecr_repo and total_deleted == len(images_to_delete):
302
307
  _delete_ecr_repository(ecr_client, repo_name, result)
@@ -326,16 +331,16 @@ def _delete_ecr_repository(ecr_client, repo_name: str, result: DestroyResult) ->
326
331
  # Verify repository is empty before deletion
327
332
  response = ecr_client.list_images(repositoryName=repo_name)
328
333
  remaining_images = response.get("imageIds", [])
329
-
334
+
330
335
  if remaining_images:
331
336
  result.warnings.append(f"Cannot delete ECR repository {repo_name}: repository is not empty")
332
337
  return
333
-
338
+
334
339
  # Delete the empty repository
335
340
  ecr_client.delete_repository(repositoryName=repo_name)
336
341
  result.resources_removed.append(f"ECR repository: {repo_name}")
337
342
  log.info("Deleted ECR repository: %s", repo_name)
338
-
343
+
339
344
  except ClientError as e:
340
345
  error_code = e.response["Error"]["Code"]
341
346
  if error_code == "RepositoryNotFoundException":
@@ -380,6 +385,7 @@ def _destroy_codebuild_project(
380
385
  result.warnings.append(f"Error during CodeBuild cleanup: {e}")
381
386
  log.warning("Error during CodeBuild cleanup: %s", e)
382
387
 
388
+
383
389
  def _destroy_codebuild_iam_role(
384
390
  session: boto3.Session,
385
391
  agent_config: BedrockAgentCoreAgentSchema,
@@ -390,17 +396,17 @@ def _destroy_codebuild_iam_role(
390
396
  if not agent_config.codebuild.execution_role:
391
397
  result.warnings.append("No CodeBuild execution role configured, skipping IAM cleanup")
392
398
  return
393
-
399
+
394
400
  try:
395
401
  # Note: IAM is a global service, but we specify region for consistency
396
402
  iam_client = session.client("iam", region_name=agent_config.aws.region)
397
403
  role_arn = agent_config.codebuild.execution_role
398
404
  role_name = role_arn.split("/")[-1]
399
-
405
+
400
406
  if dry_run:
401
407
  result.resources_removed.append(f"CodeBuild IAM role: {role_name} (DRY RUN)")
402
408
  return
403
-
409
+
404
410
  # Detach managed policies
405
411
  for policy in iam_client.list_attached_role_policies(RoleName=role_name).get("AttachedPolicies", []):
406
412
  iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy["PolicyArn"])
@@ -415,7 +421,7 @@ def _destroy_codebuild_iam_role(
415
421
  iam_client.delete_role(RoleName=role_name)
416
422
  result.resources_removed.append(f"Deleted CodeBuild IAM role: {role_name}")
417
423
  log.info("Deleted CodeBuild IAM role: %s", role_name)
418
-
424
+
419
425
  except ClientError as e:
420
426
  result.warnings.append(f"Failed to delete CodeBuild role {role_name}: {e}")
421
427
  log.warning("Failed to delete CodeBuild role %s: %s", role_name, e)
@@ -423,6 +429,7 @@ def _destroy_codebuild_iam_role(
423
429
  result.warnings.append(f"Error during CodeBuild IAM role cleanup: {e}")
424
430
  log.error("Error during CodeBuild IAM role cleanup: %s", e)
425
431
 
432
+
426
433
  def _destroy_iam_role(
427
434
  session: boto3.Session,
428
435
  project_config: BedrockAgentCoreConfigSchema,
@@ -443,7 +450,8 @@ def _destroy_iam_role(
443
450
 
444
451
  # Check if other agents use the same role
445
452
  other_agents_using_role = [
446
- name for name, agent in project_config.agents.items()
453
+ name
454
+ for name, agent in project_config.agents.items()
447
455
  if name != agent_config.name and agent.aws.execution_role == role_arn
448
456
  ]
449
457
 
@@ -462,10 +470,7 @@ def _destroy_iam_role(
462
470
  try:
463
471
  policies = iam_client.list_attached_role_policies(RoleName=role_name)
464
472
  for policy in policies.get("AttachedPolicies", []):
465
- iam_client.detach_role_policy(
466
- RoleName=role_name,
467
- PolicyArn=policy["PolicyArn"]
468
- )
473
+ iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy["PolicyArn"])
469
474
  except ClientError:
470
475
  pass # Continue if policy detachment fails
471
476
 
@@ -508,12 +513,12 @@ def _cleanup_agent_config(
508
513
 
509
514
  # Check if this agent is the default agent
510
515
  was_default = project_config.default_agent == agent_name
511
-
516
+
512
517
  # Remove the agent entry completely
513
518
  del project_config.agents[agent_name]
514
519
  result.resources_removed.append(f"Agent configuration: {agent_name}")
515
520
  log.info("Removed agent configuration: %s", agent_name)
516
-
521
+
517
522
  # Handle default agent cleanup
518
523
  if was_default:
519
524
  if project_config.agents:
@@ -526,7 +531,7 @@ def _cleanup_agent_config(
526
531
  # No agents left, clear default
527
532
  project_config.default_agent = None
528
533
  log.info("Cleared default agent (no agents remaining)")
529
-
534
+
530
535
  # If no agents remain, remove the config file
531
536
  if not project_config.agents:
532
537
  config_path.unlink()
@@ -539,4 +544,4 @@ def _cleanup_agent_config(
539
544
 
540
545
  except Exception as e:
541
546
  result.warnings.append(f"Failed to update configuration: {e}")
542
- log.warning("Failed to update configuration: %s", e)
547
+ log.warning("Failed to update configuration: %s", e)
@@ -67,7 +67,7 @@ def invoke_bedrock_agentcore(
67
67
  )["workloadAccessToken"]
68
68
 
69
69
  # TODO: store and read port config of local running container
70
- client = LocalBedrockAgentCoreClient("http://0.0.0.0:8080")
70
+ client = LocalBedrockAgentCoreClient("http://127.0.0.1:8080")
71
71
  response = client.invoke_endpoint(session_id, payload_str, workload_access_token)
72
72
 
73
73
  else:
@@ -13,8 +13,10 @@ from botocore.exceptions import ClientError
13
13
  from ...services.codebuild import CodeBuildService
14
14
  from ...services.ecr import deploy_to_ecr, get_or_create_ecr_repository
15
15
  from ...services.runtime import BedrockAgentCoreClient
16
+ from ...services.xray import enable_transaction_search_if_needed
16
17
  from ...utils.runtime.config import load_config, save_config
17
18
  from ...utils.runtime.container import ContainerRuntime
19
+ from ...utils.runtime.logs import get_genai_observability_url
18
20
  from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
19
21
  from .create_role import get_or_create_runtime_execution_role
20
22
  from .models import LaunchResult
@@ -135,6 +137,7 @@ def _deploy_to_bedrock_agentcore(
135
137
  agent_name: str,
136
138
  ecr_uri: str,
137
139
  region: str,
140
+ account_id: str,
138
141
  env_vars: Optional[dict] = None,
139
142
  auto_update_on_conflict: bool = False,
140
143
  ):
@@ -230,6 +233,16 @@ def _deploy_to_bedrock_agentcore(
230
233
 
231
234
  log.info("✅ Agent created/updated: %s", agent_arn)
232
235
 
236
+ # Enable Transaction Search if observability is enabled
237
+ if agent_config.aws.observability.enabled:
238
+ log.info("Observability is enabled, configuring Transaction Search...")
239
+ enable_transaction_search_if_needed(region, account_id)
240
+
241
+ # Show GenAI Observability Dashboard URL whenever OTEL is enabled
242
+ console_url = get_genai_observability_url(region)
243
+ log.info("🔍 GenAI Observability Dashboard:")
244
+ log.info(" %s", console_url)
245
+
233
246
  # Wait for agent to be ready
234
247
  log.info("Polling for endpoint to be ready...")
235
248
  result = bedrock_agentcore_client.wait_for_agent_endpoint_ready(agent_id)
@@ -367,6 +380,7 @@ def launch_bedrock_agentcore(
367
380
  bedrock_agentcore_name,
368
381
  ecr_uri,
369
382
  region,
383
+ account_id,
370
384
  env_vars,
371
385
  auto_update_on_conflict,
372
386
  )
@@ -495,6 +509,7 @@ def _launch_with_codebuild(
495
509
  agent_name,
496
510
  ecr_uri,
497
511
  region,
512
+ account_id,
498
513
  env_vars=env_vars,
499
514
  auto_update_on_conflict=auto_update_on_conflict,
500
515
  )
@@ -21,6 +21,7 @@ from openapi_schema_to_json_schema import to_json_schema
21
21
 
22
22
  from ....operations.gateway import GatewayClient
23
23
  from ..utils import (
24
+ clean_gateway_or_target_name,
24
25
  clean_variable_name,
25
26
  generate_pydantic_models,
26
27
  get_base_dir,
@@ -1064,7 +1065,7 @@ class BaseBedrockTranslator:
1064
1065
  )
1065
1066
  response_content_code = "str(agent_result)" if platform == "strands" else "agent_result[-1].content"
1066
1067
  url_pattern = self._get_url_regex_pattern()
1067
-
1068
+
1068
1069
  entrypoint_code += f"""
1069
1070
  def endpoint(payload, context):
1070
1071
  try:
@@ -1179,7 +1180,7 @@ class BaseBedrockTranslator:
1179
1180
  continue
1180
1181
 
1181
1182
  action_group_name = ag.get("actionGroupName", "AG")
1182
- clean_action_group_name = clean_variable_name(action_group_name)
1183
+ clean_action_group_name = clean_gateway_or_target_name(action_group_name)
1183
1184
  action_group_desc = ag.get("description", "").replace('"', '\\"')
1184
1185
  end_lambda_arn = ag.get("actionGroupExecutor", {}).get("lambda", "")
1185
1186
  tools = []
@@ -1323,7 +1324,7 @@ class BaseBedrockTranslator:
1323
1324
  "lambdaRegion": end_lambda_arn.split(":")[3] if end_lambda_arn else "us-west-2",
1324
1325
  }
1325
1326
 
1326
- func_desc = func_spec.get("description", "No Description Provided.")
1327
+ func_desc = func.get("description", "No Description Provided.")
1327
1328
  func_desc += f"\\nThis tool is part of the group of tools called {action_group_name}{f' (description: {action_group_desc})' if action_group_desc else ''}."
1328
1329
 
1329
1330
  func_parameters = func.get("parameters", {})
@@ -50,6 +50,20 @@ def clean_variable_name(text):
50
50
  return cleaned
51
51
 
52
52
 
53
+ def clean_gateway_or_target_name(text):
54
+ """Clean a string to create a valid Gateway or Target name."""
55
+ text = str(text)
56
+ cleaned = re.sub(r"[^a-zA-Z0-9\s]", " ", text)
57
+ cleaned = cleaned.lower()
58
+ cleaned = re.sub(r"\s+", " ", cleaned)
59
+ cleaned = cleaned.strip()
60
+ cleaned = cleaned.replace(" ", "-")
61
+ if not cleaned:
62
+ cleaned = "gateway-or-target"
63
+
64
+ return cleaned
65
+
66
+
53
67
  def unindent_by_one(input_code, spaces_per_indent=4):
54
68
  """Unindents the input code by one level of indentation.
55
69
 
@@ -399,7 +399,11 @@ class BedrockAgentCoreClient:
399
399
  agentRuntimeId=agent_id,
400
400
  endpointName=endpoint_name,
401
401
  )
402
- self.logger.info("Successfully initiated deletion of endpoint '%s' for agent ID: %s", endpoint_name, agent_id)
402
+ self.logger.info(
403
+ "Successfully initiated deletion of endpoint '%s' for agent ID: %s",
404
+ endpoint_name,
405
+ agent_id,
406
+ )
403
407
  return response
404
408
  except Exception as e:
405
409
  self.logger.error("Failed to delete endpoint '%s' for agent ID '%s': %s", endpoint_name, agent_id, str(e))
@@ -0,0 +1,161 @@
1
+ """X-Ray Transaction Search service for enabling observability."""
2
+
3
+ import json
4
+ import logging
5
+
6
+ import boto3
7
+ from botocore.exceptions import ClientError
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def _need_resource_policy(logs_client, policy_name="TransactionSearchXRayAccess"):
13
+ """Check if resource policy needs to be created (fail-safe)."""
14
+ try:
15
+ response = logs_client.describe_resource_policies()
16
+ for policy in response.get("resourcePolicies", []):
17
+ if policy.get("policyName") == policy_name:
18
+ return False # Already exists
19
+ return True # Needs creation
20
+ except Exception:
21
+ return True # If check fails, assume we need it (safe)
22
+
23
+
24
+ def _need_trace_destination(xray_client):
25
+ """Check if trace destination needs to be set (fail-safe)."""
26
+ try:
27
+ response = xray_client.get_trace_segment_destination()
28
+ return response.get("Destination") != "CloudWatchLogs"
29
+ except Exception:
30
+ return True # If check fails, assume we need it (safe)
31
+
32
+
33
+ def _need_indexing_rule(xray_client):
34
+ """Check if indexing rule needs to be configured (fail-safe)."""
35
+ try:
36
+ response = xray_client.get_indexing_rules()
37
+ for rule in response.get("IndexingRules", []):
38
+ if rule.get("Name") == "Default":
39
+ return False # Already configured
40
+ return True # Needs configuration
41
+ except Exception:
42
+ return True # If check fails, assume we need it (safe)
43
+
44
+
45
+ def enable_transaction_search_if_needed(region: str, account_id: str) -> bool:
46
+ """Enable X-Ray Transaction Search components that are not already configured.
47
+
48
+ This function checks what's already configured and only runs needed steps.
49
+ It's fail-safe - if checks fail, it assumes configuration is needed.
50
+
51
+ Args:
52
+ region: AWS region
53
+ account_id: AWS account ID
54
+
55
+ Returns:
56
+ bool: True if Transaction Search was configured successfully, False if failed
57
+ """
58
+ try:
59
+ session = boto3.Session(region_name=region)
60
+ logs_client = session.client("logs")
61
+ xray_client = session.client("xray")
62
+
63
+ steps_run = []
64
+
65
+ # Step 1: Resource policy (only if needed)
66
+ if _need_resource_policy(logs_client):
67
+ _create_cloudwatch_logs_resource_policy(logs_client, account_id, region)
68
+ steps_run.append("resource_policy")
69
+ else:
70
+ logger.info("CloudWatch Logs resource policy already configured")
71
+
72
+ # Step 2: Trace destination (only if needed)
73
+ if _need_trace_destination(xray_client):
74
+ _configure_trace_segment_destination(xray_client)
75
+ steps_run.append("trace_destination")
76
+ else:
77
+ logger.info("X-Ray trace destination already configured")
78
+
79
+ # Step 3: Indexing rule (only if needed)
80
+ if _need_indexing_rule(xray_client):
81
+ _configure_indexing_rule(xray_client)
82
+ steps_run.append("indexing_rule")
83
+ else:
84
+ logger.info("X-Ray indexing rule already configured")
85
+
86
+ if steps_run:
87
+ logger.info("✅ Transaction Search configured: %s", ", ".join(steps_run))
88
+ else:
89
+ logger.info("✅ Transaction Search already fully configured")
90
+
91
+ return True
92
+
93
+ except Exception as e:
94
+ logger.warning("Transaction Search configuration failed: %s", str(e))
95
+ logger.info("Agent launch will continue without Transaction Search")
96
+ return False # Don't fail launch
97
+
98
+
99
+ def _create_cloudwatch_logs_resource_policy(logs_client, account_id: str, region: str) -> None:
100
+ """Create CloudWatch Logs resource policy for X-Ray access (idempotent)."""
101
+ policy_name = "TransactionSearchXRayAccess"
102
+
103
+ policy_document = {
104
+ "Version": "2012-10-17",
105
+ "Statement": [
106
+ {
107
+ "Sid": "TransactionSearchXRayAccess",
108
+ "Effect": "Allow",
109
+ "Principal": {"Service": "xray.amazonaws.com"},
110
+ "Action": "logs:PutLogEvents",
111
+ "Resource": [
112
+ f"arn:aws:logs:{region}:{account_id}:log-group:aws/spans:*",
113
+ f"arn:aws:logs:{region}:{account_id}:log-group:/aws/application-signals/data:*",
114
+ ],
115
+ "Condition": {
116
+ "ArnLike": {"aws:SourceArn": f"arn:aws:xray:{region}:{account_id}:*"},
117
+ "StringEquals": {"aws:SourceAccount": account_id},
118
+ },
119
+ }
120
+ ],
121
+ }
122
+
123
+ try:
124
+ logs_client.put_resource_policy(policyName=policy_name, policyDocument=json.dumps(policy_document))
125
+ logger.info("Created/updated CloudWatch Logs resource policy")
126
+ except ClientError as e:
127
+ if e.response["Error"]["Code"] == "InvalidParameterException":
128
+ # Policy might already exist with same content
129
+ logger.info("CloudWatch Logs resource policy already configured")
130
+ else:
131
+ raise
132
+
133
+
134
+ def _configure_trace_segment_destination(xray_client) -> None:
135
+ """Configure X-Ray trace segment destination to CloudWatch Logs (idempotent)."""
136
+ try:
137
+ # Configure trace segments to be sent to CloudWatch Logs
138
+ # This enables Transaction Search functionality
139
+ xray_client.update_trace_segment_destination(Destination="CloudWatchLogs")
140
+ logger.info("Configured X-Ray trace segment destination to CloudWatch Logs")
141
+ except ClientError as e:
142
+ if e.response["Error"]["Code"] == "InvalidRequestException":
143
+ # Destination might already be configured
144
+ logger.info("X-Ray trace segment destination already configured")
145
+ else:
146
+ raise
147
+
148
+
149
+ def _configure_indexing_rule(xray_client) -> None:
150
+ """Configure X-Ray indexing rule for transaction search (idempotent)."""
151
+ try:
152
+ # Update the default indexing rule with probabilistic sampling
153
+ # This is idempotent - it will update the existing rule
154
+ xray_client.update_indexing_rule(Name="Default", Rule={"Probabilistic": {"DesiredSamplingPercentage": 1}})
155
+ logger.info("Updated X-Ray indexing rule for Transaction Search")
156
+ except ClientError as e:
157
+ if e.response["Error"]["Code"] == "InvalidRequestException":
158
+ # Rule might already be configured
159
+ logger.info("X-Ray indexing rule already configured")
160
+ else:
161
+ raise
@@ -4,6 +4,18 @@ from datetime import datetime, timezone
4
4
  from typing import Optional, Tuple
5
5
 
6
6
 
7
+ def get_genai_observability_url(region: str) -> str:
8
+ """Get GenAI Observability Dashboard console URL.
9
+
10
+ Args:
11
+ region: The AWS region
12
+
13
+ Returns:
14
+ The GenAI Observability Dashboard console URL
15
+ """
16
+ return f"https://console.aws.amazon.com/cloudwatch/home?region={region}#gen-ai-observability/agent-core"
17
+
18
+
7
19
  def get_agent_log_paths(agent_id: str, endpoint_name: Optional[str] = None) -> Tuple[str, str]:
8
20
  """Get CloudWatch log group paths for an agent.
9
21