agentex-sdk 0.4.0__py3-none-any.whl → 0.4.1__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 (25) hide show
  1. agentex/_version.py +1 -1
  2. agentex/lib/cli/commands/agents.py +34 -31
  3. agentex/lib/cli/commands/init.py +1 -0
  4. agentex/lib/cli/handlers/deploy_handlers.py +47 -89
  5. agentex/lib/cli/handlers/run_handlers.py +11 -7
  6. agentex/lib/cli/templates/default/environments.yaml.j2 +57 -0
  7. agentex/lib/cli/templates/default/manifest.yaml.j2 +4 -0
  8. agentex/lib/cli/templates/sync/environments.yaml.j2 +53 -0
  9. agentex/lib/cli/templates/temporal/environments.yaml.j2 +64 -0
  10. agentex/lib/cli/templates/temporal/manifest.yaml.j2 +4 -1
  11. agentex/lib/cli/utils/auth_utils.py +45 -5
  12. agentex/lib/core/services/adk/providers/openai.py +1 -1
  13. agentex/lib/sdk/config/agent_manifest.py +20 -2
  14. agentex/lib/sdk/config/environment_config.py +188 -0
  15. agentex/lib/sdk/config/validation.py +252 -0
  16. {agentex_sdk-0.4.0.dist-info → agentex_sdk-0.4.1.dist-info}/METADATA +2 -2
  17. {agentex_sdk-0.4.0.dist-info → agentex_sdk-0.4.1.dist-info}/RECORD +20 -20
  18. agentex/lib/cli/templates/default/deploy/example.yaml.j2 +0 -55
  19. agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
  20. agentex/lib/cli/templates/deploy/example.yaml.j2 +0 -55
  21. agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +0 -55
  22. agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +0 -55
  23. {agentex_sdk-0.4.0.dist-info → agentex_sdk-0.4.1.dist-info}/WHEEL +0 -0
  24. {agentex_sdk-0.4.0.dist-info → agentex_sdk-0.4.1.dist-info}/entry_points.txt +0 -0
  25. {agentex_sdk-0.4.0.dist-info → agentex_sdk-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,56 @@
1
1
  import base64
2
2
  import json
3
+ from typing import Any, Dict
3
4
 
4
5
  from agentex.lib.sdk.config.agent_manifest import AgentManifest
6
+ from agentex.lib.sdk.config.environment_config import AgentAuthConfig
5
7
 
8
+ # DEPRECATED: Old function for backward compatibility
9
+ # Will be removed in future version
10
+ def _encode_principal_context(manifest: AgentManifest) -> str | None:
11
+ """
12
+ DEPRECATED: This function is deprecated as AgentManifest no longer contains auth.
13
+ Use _encode_principal_context_from_env_config instead.
14
+
15
+ This function is kept temporarily for backward compatibility during migration.
16
+ """
17
+ # AgentManifest no longer has auth field - this will always return None
18
+ return None
6
19
 
7
- # Base 64 encode principal dictionary
8
- def _encode_principal_context(manifest: AgentManifest):
9
- if manifest.auth is None:
20
+
21
+ def _encode_principal_context_from_env_config(auth_config: "AgentAuthConfig | None") -> str | None:
22
+ """
23
+ Encode principal context from environment configuration.
24
+
25
+ Args:
26
+ auth_config: AgentAuthConfig containing principal configuration
27
+
28
+ Returns:
29
+ Base64-encoded JSON string of the principal, or None if no principal
30
+ """
31
+ if auth_config is None:
32
+ return None
33
+
34
+ principal = auth_config.principal
35
+ if not principal:
10
36
  return None
11
37
 
12
- principal = manifest.auth.principal
13
- if principal is None:
38
+ json_str = json.dumps(principal, separators=(',', ':'))
39
+ encoded_bytes = base64.b64encode(json_str.encode('utf-8'))
40
+ return encoded_bytes.decode('utf-8')
41
+
42
+
43
+ def _encode_principal_dict(principal: Dict[str, Any]) -> str | None:
44
+ """
45
+ Encode principal dictionary directly.
46
+
47
+ Args:
48
+ principal: Dictionary containing principal configuration
49
+
50
+ Returns:
51
+ Base64-encoded JSON string of the principal, or None if principal is empty
52
+ """
53
+ if not principal:
14
54
  return None
15
55
 
16
56
  json_str = json.dumps(principal, separators=(',', ':'))
@@ -656,7 +656,7 @@ class OpenAIService:
656
656
  reasoning_content = ReasoningContent(
657
657
  author="agent",
658
658
  summary=[summary.text for summary in reasoning_item.summary],
659
- content=[content.text for content in reasoning_item.content] if reasoning_item.content else None,
659
+ content=[content.text for content in reasoning_item.content] if hasattr(reasoning_item, "content") else None,
660
660
  )
661
661
 
662
662
  # Create reasoning content using streaming context (immediate completion)
@@ -15,7 +15,8 @@ from pydantic import Field
15
15
 
16
16
  from agentex.lib.sdk.config.agent_config import AgentConfig
17
17
  from agentex.lib.sdk.config.build_config import BuildConfig
18
- from agentex.lib.sdk.config.deployment_config import DeploymentConfig, AuthenticationConfig
18
+ from agentex.lib.sdk.config.environment_config import AgentEnvironmentsConfig
19
+ from agentex.lib.sdk.config.deployment_config import DeploymentConfig
19
20
  from agentex.lib.sdk.config.local_development_config import LocalDevelopmentConfig
20
21
  from agentex.lib.utils.logging import make_logger
21
22
  from agentex.lib.utils.model_utils import BaseModel
@@ -36,7 +37,7 @@ class AgentManifest(BaseModel):
36
37
  deployment: DeploymentConfig | None = Field(
37
38
  default=None, description="Deployment configuration for the agent"
38
39
  )
39
- auth: AuthenticationConfig | None = Field(default=None, description="Authentication configuration")
40
+
40
41
 
41
42
  def context_manager(self, build_context_root: Path) -> BuildContextManager:
42
43
  """
@@ -45,6 +46,23 @@ class AgentManifest(BaseModel):
45
46
  return BuildContextManager(
46
47
  agent_manifest=self, build_context_root=build_context_root
47
48
  )
49
+
50
+ def load_environments_config(self, manifest_dir: Path) -> "AgentEnvironmentsConfig | None":
51
+ """Load environments.yaml from same directory as manifest.yaml.
52
+
53
+ Args:
54
+ manifest_dir: Directory containing manifest.yaml
55
+
56
+ Returns:
57
+ AgentEnvironmentsConfig if environments.yaml exists, None otherwise
58
+
59
+ Raises:
60
+ ValueError: If environments.yaml exists but is invalid
61
+ """
62
+ # Import here to avoid circular imports
63
+ from agentex.lib.sdk.config.environment_config import load_environments_config_from_manifest_dir
64
+
65
+ return load_environments_config_from_manifest_dir(manifest_dir)
48
66
 
49
67
 
50
68
  class BuildContextManager:
@@ -0,0 +1,188 @@
1
+ """
2
+ Environment-specific configuration models for agent deployments.
3
+
4
+ This module provides Pydantic models for managing environment-specific
5
+ configurations that are separate from the main manifest.yaml file.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any, Dict, override
12
+
13
+ import yaml
14
+ from pydantic import BaseModel, Field, field_validator
15
+
16
+ from agentex.lib.utils.model_utils import BaseModel as UtilsBaseModel
17
+
18
+
19
+ class AgentAuthConfig(BaseModel):
20
+ """Authentication configuration for an agent in a specific environment."""
21
+
22
+ principal: Dict[str, Any] = Field(
23
+ ...,
24
+ description="Principal configuration for agent authorization and registration"
25
+ )
26
+
27
+ @field_validator('principal')
28
+ @classmethod
29
+ def validate_principal_required_fields(cls, v: Any) -> Dict[str, Any]:
30
+ """Ensure principal has required fields for agent registration."""
31
+ if not isinstance(v, dict):
32
+ raise ValueError("Principal must be a dictionary")
33
+ return v
34
+
35
+
36
+ class AgentKubernetesConfig(BaseModel):
37
+ """Kubernetes configuration for an agent in a specific environment."""
38
+
39
+ namespace: str = Field(
40
+ ...,
41
+ description="Kubernetes namespace where the agent will be deployed"
42
+ )
43
+
44
+ @field_validator('namespace')
45
+ @classmethod
46
+ def validate_namespace_format(cls, v: str) -> str:
47
+ """Ensure namespace follows Kubernetes naming conventions."""
48
+ if not v or not v.strip():
49
+ raise ValueError("Namespace cannot be empty")
50
+
51
+ # Basic Kubernetes namespace validation
52
+ namespace = v.strip().lower()
53
+ if not namespace.replace('-', '').replace('.', '').isalnum():
54
+ raise ValueError(
55
+ f"Namespace '{v}' must contain only lowercase letters, numbers, "
56
+ "hyphens, and periods"
57
+ )
58
+
59
+ if len(namespace) > 63:
60
+ raise ValueError(f"Namespace '{v}' cannot exceed 63 characters")
61
+
62
+ return namespace
63
+
64
+
65
+ class AgentEnvironmentConfig(BaseModel):
66
+ """Complete configuration for an agent in a specific environment."""
67
+
68
+ kubernetes: AgentKubernetesConfig | None = Field(
69
+ default=None,
70
+ description="Kubernetes deployment configuration"
71
+ )
72
+ auth: AgentAuthConfig = Field(
73
+ ...,
74
+ description="Authentication and authorization configuration"
75
+ )
76
+ helm_overrides: Dict[str, Any] = Field(
77
+ default_factory=dict,
78
+ description="Helm chart value overrides for environment-specific tuning"
79
+ )
80
+
81
+
82
+ class AgentEnvironmentsConfig(UtilsBaseModel):
83
+ """All environment configurations for an agent."""
84
+
85
+ schema_version: str = Field(
86
+ default="v1",
87
+ description="Schema version for validation and compatibility"
88
+ )
89
+ environments: Dict[str, AgentEnvironmentConfig] = Field(
90
+ ...,
91
+ description="Environment-specific configurations (dev, prod, etc.)"
92
+ )
93
+
94
+ @field_validator('schema_version')
95
+ @classmethod
96
+ def validate_schema_version(cls, v: str) -> str:
97
+ """Ensure schema version is supported."""
98
+ supported_versions = ['v1']
99
+ if v not in supported_versions:
100
+ raise ValueError(
101
+ f"Schema version '{v}' not supported. "
102
+ f"Supported versions: {', '.join(supported_versions)}"
103
+ )
104
+ return v
105
+
106
+ @field_validator('environments')
107
+ @classmethod
108
+ def validate_environments_not_empty(cls, v: Dict[str, AgentEnvironmentConfig]) -> Dict[str, AgentEnvironmentConfig]:
109
+ """Ensure at least one environment is defined."""
110
+ if not v:
111
+ raise ValueError("At least one environment must be defined")
112
+ return v
113
+
114
+ def get_config_for_env(self, env_name: str) -> AgentEnvironmentConfig:
115
+ """Get configuration for a specific environment.
116
+
117
+ Args:
118
+ env_name: Name of the environment (e.g., 'dev', 'prod')
119
+
120
+ Returns:
121
+ AgentEnvironmentConfig for the specified environment
122
+
123
+ Raises:
124
+ ValueError: If environment is not found
125
+ """
126
+ if env_name not in self.environments:
127
+ available_envs = ', '.join(self.environments.keys())
128
+ raise ValueError(
129
+ f"Environment '{env_name}' not found in environments.yaml. "
130
+ f"Available environments: {available_envs}"
131
+ )
132
+ return self.environments[env_name]
133
+
134
+ def list_environments(self) -> list[str]:
135
+ """Get list of all configured environment names."""
136
+ return list(self.environments.keys())
137
+
138
+ @classmethod
139
+ @override
140
+ def from_yaml(cls, file_path: str) -> "AgentEnvironmentsConfig":
141
+ """Load configuration from environments.yaml file.
142
+
143
+ Args:
144
+ file_path: Path to environments.yaml file
145
+
146
+ Returns:
147
+ Parsed and validated AgentEnvironmentsConfig
148
+
149
+ Raises:
150
+ FileNotFoundError: If file doesn't exist
151
+ ValueError: If file is invalid or doesn't validate
152
+ """
153
+ path = Path(file_path)
154
+ if not path.exists():
155
+ raise FileNotFoundError(f"environments.yaml not found: {file_path}")
156
+
157
+ try:
158
+ with open(path, 'r') as f:
159
+ data = yaml.safe_load(f)
160
+
161
+ if not data:
162
+ raise ValueError("environments.yaml file is empty")
163
+
164
+ return cls.model_validate(data)
165
+
166
+ except yaml.YAMLError as e:
167
+ raise ValueError(f"Invalid YAML format in {file_path}: {e}") from e
168
+ except Exception as e:
169
+ raise ValueError(f"Failed to load environments.yaml from {file_path}: {e}") from e
170
+
171
+
172
+ def load_environments_config_from_manifest_dir(manifest_dir: Path) -> AgentEnvironmentsConfig | None:
173
+ """Helper function to load environments.yaml from same directory as manifest.yaml.
174
+
175
+ Args:
176
+ manifest_dir: Directory containing manifest.yaml
177
+
178
+ Returns:
179
+ AgentEnvironmentsConfig if environments.yaml exists, None otherwise
180
+
181
+ Raises:
182
+ ValueError: If environments.yaml exists but is invalid
183
+ """
184
+ environments_file = manifest_dir / "environments.yaml"
185
+ if not environments_file.exists():
186
+ return None
187
+
188
+ return AgentEnvironmentsConfig.from_yaml(str(environments_file))
@@ -0,0 +1,252 @@
1
+ """
2
+ Validation framework for agent configuration files.
3
+
4
+ This module provides validation functions for agent configurations,
5
+ with clear error messages and best practices enforcement.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from agentex.lib.sdk.config.environment_config import AgentEnvironmentsConfig, AgentEnvironmentConfig
12
+ from agentex.lib.utils.logging import make_logger
13
+
14
+ logger = make_logger(__name__)
15
+
16
+
17
+ class ConfigValidationError(Exception):
18
+ """Exception raised when configuration validation fails."""
19
+
20
+ def __init__(self, message: str, file_path: Optional[str] = None):
21
+ self.file_path = file_path
22
+ super().__init__(message)
23
+
24
+
25
+ class EnvironmentsValidationError(ConfigValidationError):
26
+ """Exception raised when environments.yaml validation fails."""
27
+ pass
28
+
29
+
30
+ def validate_environments_config(
31
+ environments_config: AgentEnvironmentsConfig,
32
+ required_environments: Optional[List[str]] = None
33
+ ) -> None:
34
+ """
35
+ Validate environments configuration with comprehensive checks.
36
+
37
+ Args:
38
+ environments_config: The loaded environments configuration
39
+ required_environments: List of environment names that must be present
40
+
41
+ Raises:
42
+ EnvironmentsValidationError: If validation fails
43
+ """
44
+ # Check for required environments
45
+ if required_environments:
46
+ missing_envs: List[str] = []
47
+ for env_name in required_environments:
48
+ if env_name not in environments_config.environments:
49
+ missing_envs.append(env_name)
50
+
51
+ if missing_envs:
52
+ available_envs = list(environments_config.environments.keys())
53
+ raise EnvironmentsValidationError(
54
+ f"Missing required environments: {', '.join(missing_envs)}. "
55
+ f"Available environments: {', '.join(available_envs)}"
56
+ )
57
+
58
+ # Validate each environment configuration
59
+ for env_name, env_config in environments_config.environments.items():
60
+ try:
61
+ _validate_single_environment_config(env_name, env_config)
62
+ except Exception as e:
63
+ raise EnvironmentsValidationError(
64
+ f"Environment '{env_name}' configuration error: {str(e)}"
65
+ ) from e
66
+
67
+
68
+ def _validate_single_environment_config(env_name: str, env_config: AgentEnvironmentConfig) -> None:
69
+ """
70
+ Validate a single environment configuration.
71
+
72
+ Args:
73
+ env_name: Name of the environment
74
+ env_config: AgentEnvironmentConfig instance
75
+
76
+ Raises:
77
+ ValueError: If validation fails
78
+ """
79
+ # Validate namespace naming conventions if kubernetes config exists
80
+ if env_config.kubernetes and env_config.kubernetes.namespace:
81
+ namespace = env_config.kubernetes.namespace
82
+
83
+ # Check for common namespace naming issues
84
+ if namespace != namespace.lower():
85
+ logger.warning(
86
+ f"Namespace '{namespace}' contains uppercase letters. "
87
+ "Kubernetes namespaces should be lowercase."
88
+ )
89
+
90
+ if namespace.startswith('-') or namespace.endswith('-'):
91
+ raise ValueError(
92
+ f"Namespace '{namespace}' cannot start or end with hyphens"
93
+ )
94
+
95
+ # Validate auth principal
96
+ principal = env_config.auth.principal
97
+ if not principal.get('user_id'):
98
+ raise ValueError("Auth principal must contain non-empty 'user_id'")
99
+
100
+ # Check for environment-specific user_id patterns
101
+ user_id = principal['user_id']
102
+ if isinstance(user_id, str):
103
+ if not any(env_name.lower() in user_id.lower() for env_name in ['dev', 'prod', 'staging', env_name]):
104
+ logger.warning(
105
+ f"User ID '{user_id}' doesn't contain environment indicator. "
106
+ f"Consider including '{env_name}' in the user_id for clarity."
107
+ )
108
+
109
+ # Validate helm overrides if present
110
+ if env_config.helm_overrides:
111
+ _validate_helm_overrides(env_config.helm_overrides)
112
+
113
+
114
+ def _validate_helm_overrides(helm_overrides: Dict[str, Any]) -> None:
115
+ """
116
+ Validate helm override configuration.
117
+
118
+ Args:
119
+ helm_overrides: Dictionary of helm overrides
120
+
121
+ Raises:
122
+ ValueError: If validation fails
123
+ """
124
+ # Check for common helm override issues
125
+ if 'resources' in helm_overrides:
126
+ resources = helm_overrides['resources']
127
+ if isinstance(resources, dict):
128
+ # Validate resource format
129
+ if 'requests' in resources or 'limits' in resources:
130
+ for resource_type in ['requests', 'limits']:
131
+ if resource_type in resources:
132
+ resource_config: Any = resources[resource_type]
133
+ if isinstance(resource_config, dict):
134
+ # Check for valid resource specifications
135
+ for key, value in resource_config.items():
136
+ if key in ['cpu', 'memory'] and not isinstance(value, str):
137
+ logger.warning(
138
+ f"Resource {key} should be a string (e.g., '500m', '1Gi'), "
139
+ f"got {type(value).__name__}: {value}"
140
+ )
141
+
142
+
143
+ def validate_environments_yaml_file(file_path: str) -> AgentEnvironmentsConfig:
144
+ """
145
+ Load and validate environments.yaml file.
146
+
147
+ Args:
148
+ file_path: Path to environments.yaml file
149
+
150
+ Returns:
151
+ Validated AgentEnvironmentsConfig
152
+
153
+ Raises:
154
+ EnvironmentsValidationError: If file is invalid
155
+ """
156
+ try:
157
+ environments_config = AgentEnvironmentsConfig.from_yaml(file_path)
158
+ validate_environments_config(environments_config)
159
+ return environments_config
160
+ except FileNotFoundError:
161
+ raise EnvironmentsValidationError(
162
+ f"environments.yaml not found: {file_path}\n\n"
163
+ "💡 To create one:\n"
164
+ " agentex agents init-environments\n\n"
165
+ "📋 Why required:\n"
166
+ " Environment-specific settings (auth, namespace, resources)\n"
167
+ " must be separated from global manifest for proper isolation.",
168
+ file_path=file_path
169
+ ) from None
170
+ except Exception as e:
171
+ raise EnvironmentsValidationError(
172
+ f"Invalid environments.yaml file: {str(e)}",
173
+ file_path=file_path
174
+ ) from e
175
+
176
+
177
+ def validate_manifest_and_environments(
178
+ manifest_path: str,
179
+ required_environment: Optional[str] = None
180
+ ) -> tuple[str, AgentEnvironmentsConfig]:
181
+ """
182
+ Validate both manifest.yaml and environments.yaml files together.
183
+
184
+ Args:
185
+ manifest_path: Path to manifest.yaml file
186
+ required_environment: Specific environment that must be present
187
+
188
+ Returns:
189
+ Tuple of (manifest_path, environments_config)
190
+
191
+ Raises:
192
+ ConfigValidationError: If validation fails
193
+ """
194
+ manifest_file = Path(manifest_path)
195
+ if not manifest_file.exists():
196
+ raise ConfigValidationError(f"Manifest file not found: {manifest_path}")
197
+
198
+ # Look for environments.yaml in same directory
199
+ environments_file = manifest_file.parent / "environments.yaml"
200
+ environments_config = validate_environments_yaml_file(str(environments_file))
201
+
202
+ # Validate specific environment if requested
203
+ if required_environment:
204
+ validate_environments_config(
205
+ environments_config,
206
+ required_environments=[required_environment]
207
+ )
208
+
209
+ return manifest_path, environments_config
210
+
211
+
212
+ def generate_helpful_error_message(error: Exception, context: str = "") -> str:
213
+ """
214
+ Generate helpful error message with troubleshooting tips.
215
+
216
+ Args:
217
+ error: The original exception
218
+ context: Additional context about where the error occurred
219
+
220
+ Returns:
221
+ Formatted error message with troubleshooting tips
222
+ """
223
+ base_msg = str(error)
224
+
225
+ if context:
226
+ base_msg = f"{context}: {base_msg}"
227
+
228
+ # Add troubleshooting tips based on error type
229
+ if isinstance(error, FileNotFoundError):
230
+ if "environments.yaml" in base_msg:
231
+ base_msg += (
232
+ "\n\n🔧 Troubleshooting:\n"
233
+ "1. Create environments.yaml: agentex agents init-environments\n"
234
+ "2. Check file location: should be next to manifest.yaml\n"
235
+ "3. Verify file permissions"
236
+ )
237
+ elif "user_id" in base_msg.lower():
238
+ base_msg += (
239
+ "\n\n💡 Auth Principal Tips:\n"
240
+ "- user_id should be unique per environment\n"
241
+ "- Include environment name (e.g., 'dev_my_agent')\n"
242
+ "- Use consistent naming convention across agents"
243
+ )
244
+ elif "namespace" in base_msg.lower():
245
+ base_msg += (
246
+ "\n\n🏷️ Namespace Tips:\n"
247
+ "- Use lowercase letters, numbers, and hyphens only\n"
248
+ "- Include team and environment (e.g., 'team-dev-agent')\n"
249
+ "- Keep under 63 characters"
250
+ )
251
+
252
+ return base_msg
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: agentex-sdk
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: The official Python library for the agentex API
5
5
  Project-URL: Homepage, https://github.com/scaleapi/agentex-python
6
6
  Project-URL: Repository, https://github.com/scaleapi/agentex-python
@@ -31,7 +31,7 @@ Requires-Dist: jsonschema<5,>=4.23.0
31
31
  Requires-Dist: kubernetes<29.0.0,>=25.0.0
32
32
  Requires-Dist: litellm<2,>=1.66.0
33
33
  Requires-Dist: mcp[cli]>=1.4.1
34
- Requires-Dist: openai-agents!=0.2.3,>=0.0.7
34
+ Requires-Dist: openai-agents==0.2.7
35
35
  Requires-Dist: openai>=1.99.9
36
36
  Requires-Dist: pydantic<3,>=2.0.0
37
37
  Requires-Dist: pytest-asyncio>=1.0.0