agentex-sdk 0.8.1__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. agentex/_base_client.py +134 -11
  2. agentex/_models.py +16 -1
  3. agentex/_types.py +9 -0
  4. agentex/_version.py +1 -1
  5. agentex/lib/cli/commands/agents.py +141 -73
  6. agentex/lib/cli/commands/init.py +13 -2
  7. agentex/lib/cli/handlers/agent_handlers.py +130 -12
  8. agentex/lib/cli/templates/sync-openai-agents/.dockerignore.j2 +43 -0
  9. agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 +42 -0
  10. agentex/lib/cli/templates/sync-openai-agents/Dockerfile.j2 +43 -0
  11. agentex/lib/cli/templates/sync-openai-agents/README.md.j2 +313 -0
  12. agentex/lib/cli/templates/sync-openai-agents/dev.ipynb.j2 +167 -0
  13. agentex/lib/cli/templates/sync-openai-agents/environments.yaml.j2 +53 -0
  14. agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 +115 -0
  15. agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 +137 -0
  16. agentex/lib/cli/templates/sync-openai-agents/pyproject.toml.j2 +32 -0
  17. agentex/lib/cli/templates/sync-openai-agents/requirements.txt.j2 +5 -0
  18. agentex/lib/cli/templates/sync-openai-agents/test_agent.py.j2 +70 -0
  19. agentex/lib/sdk/config/environment_config.py +113 -73
  20. agentex/lib/sdk/config/validation.py +62 -61
  21. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/METADATA +1 -1
  22. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/RECORD +25 -14
  23. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/licenses/LICENSE +1 -1
  24. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/WHEEL +0 -0
  25. {agentex_sdk-0.8.1.dist-info → agentex_sdk-0.9.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,137 @@
1
+ import os
2
+ from typing import AsyncGenerator, List
3
+
4
+ from agentex.lib import adk
5
+ from agentex.lib.adk.providers._modules.sync_provider import SyncStreamingProvider, convert_openai_to_agentex_events
6
+ from agentex.lib.sdk.fastacp.fastacp import FastACP
7
+ from agentex.lib.types.acp import SendMessageParams
8
+ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
9
+ from agentex.lib.types.tracing import SGPTracingProcessorConfig
10
+ from agentex.lib.utils.model_utils import BaseModel
11
+
12
+ from agentex.types.task_message_update import TaskMessageUpdate
13
+ from agentex.types.task_message_content import TaskMessageContent
14
+ from agentex.lib.utils.logging import make_logger
15
+ from agents import Agent, Runner, RunConfig, function_tool
16
+
17
+
18
+ logger = make_logger(__name__)
19
+
20
+ SGP_API_KEY = os.environ.get("SGP_API_KEY", "")
21
+ SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "")
22
+
23
+ if SGP_API_KEY and SGP_ACCOUNT_ID:
24
+ add_tracing_processor_config(
25
+ SGPTracingProcessorConfig(
26
+ sgp_api_key=SGP_API_KEY,
27
+ sgp_account_id=SGP_ACCOUNT_ID,
28
+ )
29
+ )
30
+
31
+
32
+ MODEL = "gpt-4o-mini"
33
+
34
+ SYSTEM_PROMPT = """
35
+ <role>
36
+ You are a helpful assistant. Use your tools to help the user.
37
+ </role>
38
+
39
+ <communication_style>
40
+ Communicate in a witty and friendly manner
41
+ </communication_style>
42
+ """
43
+
44
+ AGENT_NAME = "{{ agent_name }}"
45
+
46
+
47
+ @function_tool
48
+ async def get_weather() -> str:
49
+ """
50
+ Get the current weather.
51
+
52
+ This is a dummy activity that returns a hardcoded string for demo purposes.
53
+ Replace this with a real weather API call in your implementation.
54
+
55
+ Returns:
56
+ A string describing the current weather conditions.
57
+ """
58
+ logger.info("get_weather activity called")
59
+ return "Sunny, 72°F"
60
+
61
+
62
+
63
+ # Create an ACP server
64
+ acp = FastACP.create(
65
+ acp_type="sync",
66
+ )
67
+
68
+ class StateModel(BaseModel):
69
+ input_list: List[dict]
70
+ turn_number: int
71
+
72
+
73
+ @acp.on_message_send
74
+ async def handle_message_send(
75
+ params: SendMessageParams
76
+ ) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]:
77
+ if not os.environ.get("OPENAI_API_KEY"):
78
+ yield StreamTaskMessageFull(
79
+ index=0,
80
+ type="full",
81
+ content=TextContent(
82
+ author="agent",
83
+ content="Hey, sorry I'm unable to respond to your message because you're running this example without an OpenAI API key. Please set the OPENAI_API_KEY environment variable to run this example. Do this by either by adding a .env file to the project/ directory or by setting the environment variable in your terminal.",
84
+ ),
85
+ )
86
+
87
+ user_prompt = params.content.content
88
+
89
+ # Retrieve the task state. Each event is handled as a new turn, so we need to get the state for the current turn.
90
+ task_state = await adk.state.get_by_task_and_agent(task_id=params.task.id, agent_id=params.agent.id)
91
+ if not task_state:
92
+ # If the state doesn't exist, create it.
93
+ state = StateModel(input_list=[], turn_number=0)
94
+ task_state = await adk.state.create(task_id=params.task.id, agent_id=params.agent.id, state=state)
95
+ else:
96
+ state = StateModel.model_validate(task_state.state)
97
+
98
+ state.turn_number += 1
99
+ state.input_list.append({"role": "user", "content": user_prompt})
100
+
101
+ # Initialize the sync provider and run config to allow for tracing
102
+ provider = SyncStreamingProvider(
103
+ trace_id=params.task.id,
104
+ )
105
+
106
+ run_config = RunConfig(
107
+ model_provider=provider,
108
+ )
109
+
110
+ # Initialize the agent
111
+ agent = Agent(
112
+ name=AGENT_NAME,
113
+ instructions=SYSTEM_PROMPT,
114
+ model=MODEL,
115
+ tools=[get_weather],
116
+ )
117
+
118
+ # Run the agent with the conversation history from state
119
+ result = Runner.run_streamed(
120
+ agent,
121
+ state.input_list,
122
+ run_config=run_config
123
+ )
124
+
125
+ # Convert the OpenAI events to Agentex events and stream them back to the client
126
+ async for agentex_event in convert_openai_to_agentex_events(result.stream_events()):
127
+ yield agentex_event
128
+
129
+ # After streaming is complete, update state with the full conversation history
130
+ state.input_list = result.to_input_list()
131
+ await adk.state.update(
132
+ state_id=task_state.id,
133
+ task_id=params.task.id,
134
+ agent_id=params.agent.id,
135
+ state=state,
136
+ trace_id=params.task.id,
137
+ )
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "{{ project_name }}"
7
+ version = "0.1.0"
8
+ description = "{{ description }}"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "agentex-sdk",
12
+ "scale-gp",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "pytest",
18
+ "black",
19
+ "isort",
20
+ "flake8",
21
+ ]
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["project"]
25
+
26
+ [tool.black]
27
+ line-length = 88
28
+ target-version = ['py312']
29
+
30
+ [tool.isort]
31
+ profile = "black"
32
+ line_length = 88
@@ -0,0 +1,5 @@
1
+ # Install agentex-sdk from local path
2
+ agentex-sdk
3
+
4
+ # Scale GenAI Platform Python SDK
5
+ scale-gp
@@ -0,0 +1,70 @@
1
+ """
2
+ Sample tests for AgentEx ACP agent.
3
+
4
+ This test suite demonstrates how to test the main AgentEx API functions:
5
+ - Non-streaming message sending
6
+ - Streaming message sending
7
+ - Task creation via RPC
8
+
9
+ To run these tests:
10
+ 1. Make sure the agent is running (via docker-compose or `agentex agents run`)
11
+ 2. Set the AGENTEX_API_BASE_URL environment variable if not using default
12
+ 3. Run: pytest test_agent.py -v
13
+
14
+ Configuration:
15
+ - AGENTEX_API_BASE_URL: Base URL for the AgentEx server (default: http://localhost:5003)
16
+ - AGENT_NAME: Name of the agent to test (default: {{ agent_name }})
17
+ """
18
+
19
+ import os
20
+ import pytest
21
+ from agentex import Agentex
22
+
23
+
24
+ # Configuration from environment variables
25
+ AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003")
26
+ AGENT_NAME = os.environ.get("AGENT_NAME", "{{ agent_name }}")
27
+
28
+
29
+ @pytest.fixture
30
+ def client():
31
+ """Create an AgentEx client instance for testing."""
32
+ return Agentex(base_url=AGENTEX_API_BASE_URL)
33
+
34
+
35
+ @pytest.fixture
36
+ def agent_name():
37
+ """Return the agent name for testing."""
38
+ return AGENT_NAME
39
+
40
+
41
+ @pytest.fixture
42
+ def agent_id(client, agent_name):
43
+ """Retrieve the agent ID based on the agent name."""
44
+ agents = client.agents.list()
45
+ for agent in agents:
46
+ if agent.name == agent_name:
47
+ return agent.id
48
+ raise ValueError(f"Agent with name {agent_name} not found.")
49
+
50
+
51
+ class TestNonStreamingMessages:
52
+ """Test non-streaming message sending."""
53
+
54
+ def test_send_message(self, client: Agentex, _agent_name: str):
55
+ """Test sending a message and receiving a response."""
56
+ # TODO: Fill in the test based on what data your agent is expected to handle
57
+ ...
58
+
59
+
60
+ class TestStreamingMessages:
61
+ """Test streaming message sending."""
62
+
63
+ def test_send_stream_message(self, client: Agentex, _agent_name: str):
64
+ """Test streaming a message and aggregating deltas."""
65
+ # TODO: Fill in the test based on what data your agent is expected to handle
66
+ ...
67
+
68
+
69
+ if __name__ == "__main__":
70
+ pytest.main([__file__, "-v"])
@@ -18,13 +18,12 @@ from agentex.lib.utils.model_utils import BaseModel as UtilsBaseModel
18
18
 
19
19
  class AgentAuthConfig(BaseModel):
20
20
  """Authentication configuration for an agent in a specific environment."""
21
-
21
+
22
22
  principal: Dict[str, Any] = Field(
23
- ...,
24
- description="Principal configuration for agent authorization and registration"
23
+ ..., description="Principal configuration for agent authorization and registration"
25
24
  )
26
-
27
- @field_validator('principal')
25
+
26
+ @field_validator("principal")
28
27
  @classmethod
29
28
  def validate_principal_required_fields(cls, v: Any) -> Dict[str, Any]:
30
29
  """Ensure principal has required fields for agent registration."""
@@ -35,125 +34,166 @@ class AgentAuthConfig(BaseModel):
35
34
 
36
35
  class AgentKubernetesConfig(BaseModel):
37
36
  """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')
37
+
38
+ namespace: str = Field(..., description="Kubernetes namespace where the agent will be deployed")
39
+
40
+ @field_validator("namespace")
45
41
  @classmethod
46
42
  def validate_namespace_format(cls, v: str) -> str:
47
43
  """Ensure namespace follows Kubernetes naming conventions."""
48
44
  if not v or not v.strip():
49
45
  raise ValueError("Namespace cannot be empty")
50
-
46
+
51
47
  # Basic Kubernetes namespace validation
52
48
  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
-
49
+ if not namespace.replace("-", "").replace(".", "").isalnum():
50
+ raise ValueError(f"Namespace '{v}' must contain only lowercase letters, numbers, hyphens, and periods")
51
+
59
52
  if len(namespace) > 63:
60
53
  raise ValueError(f"Namespace '{v}' cannot exceed 63 characters")
61
-
54
+
62
55
  return namespace
63
56
 
64
57
 
65
58
  class AgentEnvironmentConfig(BaseModel):
66
59
  """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_repository_name: str = Field(
77
- default="scale-egp",
78
- description="Helm repository name for the environment"
60
+
61
+ kubernetes: AgentKubernetesConfig | None = Field(default=None, description="Kubernetes deployment configuration")
62
+ environment: str | None = Field(
63
+ default=None,
64
+ description="The environment keyword that this specific environment maps to: either dev, staging, prod",
79
65
  )
66
+ auth: AgentAuthConfig = Field(..., description="Authentication and authorization configuration")
67
+ helm_repository_name: str = Field(default="scale-egp", description="Helm repository name for the environment")
80
68
  helm_repository_url: str = Field(
81
- default="https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts",
82
- description="Helm repository url for the environment"
69
+ default="https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts",
70
+ description="Helm repository url for the environment",
83
71
  )
84
72
  helm_overrides: Dict[str, Any] = Field(
85
- default_factory=dict,
86
- description="Helm chart value overrides for environment-specific tuning"
73
+ default_factory=dict, description="Helm chart value overrides for environment-specific tuning"
87
74
  )
88
75
 
89
76
 
90
77
  class AgentEnvironmentsConfig(UtilsBaseModel):
91
78
  """All environment configurations for an agent."""
92
-
93
- schema_version: str = Field(
94
- default="v1",
95
- description="Schema version for validation and compatibility"
96
- )
79
+
80
+ schema_version: str = Field(default="v1", description="Schema version for validation and compatibility")
97
81
  environments: Dict[str, AgentEnvironmentConfig] = Field(
98
- ...,
99
- description="Environment-specific configurations (dev, prod, etc.)"
82
+ ..., description="Environment-specific configurations (dev, prod, etc.)"
100
83
  )
101
-
102
- @field_validator('schema_version')
84
+
85
+ @field_validator("schema_version")
103
86
  @classmethod
104
87
  def validate_schema_version(cls, v: str) -> str:
105
88
  """Ensure schema version is supported."""
106
- supported_versions = ['v1']
89
+ supported_versions = ["v1"]
107
90
  if v not in supported_versions:
108
- raise ValueError(
109
- f"Schema version '{v}' not supported. "
110
- f"Supported versions: {', '.join(supported_versions)}"
111
- )
91
+ raise ValueError(f"Schema version '{v}' not supported. Supported versions: {', '.join(supported_versions)}")
112
92
  return v
113
-
114
- @field_validator('environments')
93
+
94
+ @field_validator("environments")
115
95
  @classmethod
116
96
  def validate_environments_not_empty(cls, v: Dict[str, AgentEnvironmentConfig]) -> Dict[str, AgentEnvironmentConfig]:
117
97
  """Ensure at least one environment is defined."""
118
98
  if not v:
119
99
  raise ValueError("At least one environment must be defined")
120
100
  return v
121
-
101
+
122
102
  def get_config_for_env(self, env_name: str) -> AgentEnvironmentConfig:
123
103
  """Get configuration for a specific environment.
124
-
104
+
125
105
  Args:
126
106
  env_name: Name of the environment (e.g., 'dev', 'prod')
127
-
107
+
128
108
  Returns:
129
109
  AgentEnvironmentConfig for the specified environment
130
-
110
+
131
111
  Raises:
132
112
  ValueError: If environment is not found
133
113
  """
134
114
  if env_name not in self.environments:
135
- available_envs = ', '.join(self.environments.keys())
115
+ available_envs = ", ".join(self.environments.keys())
136
116
  raise ValueError(
137
- f"Environment '{env_name}' not found in environments.yaml. "
138
- f"Available environments: {available_envs}"
117
+ f"Environment '{env_name}' not found in environments.yaml. Available environments: {available_envs}"
139
118
  )
140
119
  return self.environments[env_name]
141
-
120
+
121
+ def get_configs_for_env(self, env_target: str) -> dict[str, AgentEnvironmentConfig]:
122
+ """Get configuration for a specific environment based on the expected mapping.
123
+ The environment is either:
124
+ 1. explicitly specified like so using a key-map in the environments conifg:
125
+ environments:
126
+ dev-aws:
127
+ environment: "dev"
128
+ kubernetes:
129
+ namespace: "sgp-000-hello-acp"
130
+ auth:
131
+ principal:
132
+ user_id: 73d0c8bd-4726-434c-9686-eb627d89f078
133
+ account_id: 6887f093600ecd59bbbd3095
134
+ helm_overrides:
135
+
136
+ or: it it can be defined at the top level:
137
+ dev:
138
+ kubernetes:
139
+ namespace: "sgp-000-hello-acp"
140
+ auth:
141
+ principal:
142
+ user_id: 73d0c8bd-4726-434c-9686-eb627d89f078
143
+ account_id: 6887f093600ecd59bbbd3095
144
+ helm_overrides:
145
+
146
+ if the environment field is not explicitly set, we assume its the same as
147
+ the name of the environment
148
+ Args:
149
+ env_target: Name of the environment target (e.g., 'dev', 'prod')
150
+
151
+ Returns:
152
+ AgentEnvironmentConfig for the specified environment
153
+
154
+ Raises:
155
+ ValueError: If environment is not found
156
+ """
157
+ envs_to_deploy = {}
158
+ if env_target in self.environments:
159
+ # this supports if the top-level key is just "dev, staging, etc" and matches
160
+ # the environment name exactly without any explicit mapping
161
+ envs_to_deploy[env_target] = self.environments[env_target]
162
+
163
+ for env_name, config in self.environments.items():
164
+ if config.environment == env_target:
165
+ envs_to_deploy[env_name] = config
166
+
167
+ if len(envs_to_deploy) == 0:
168
+ ## this just finds environments for each target, so "available_envs" refers to each target environment
169
+
170
+ available_envs = set()
171
+ for env_name, config in self.environments.items():
172
+ if config.environment is not None:
173
+ available_envs.add(config.environment)
174
+ else:
175
+ available_envs.add(env_name)
176
+ raise ValueError(
177
+ f"Environment '{env_target}' not found in environments.yaml. Available environments: {available_envs}"
178
+ )
179
+
180
+ return envs_to_deploy
181
+
142
182
  def list_environments(self) -> list[str]:
143
183
  """Get list of all configured environment names."""
144
184
  return list(self.environments.keys())
145
-
185
+
146
186
  @classmethod
147
187
  @override
148
188
  def from_yaml(cls, file_path: str) -> "AgentEnvironmentsConfig":
149
189
  """Load configuration from environments.yaml file.
150
-
190
+
151
191
  Args:
152
192
  file_path: Path to environments.yaml file
153
-
193
+
154
194
  Returns:
155
195
  Parsed and validated AgentEnvironmentsConfig
156
-
196
+
157
197
  Raises:
158
198
  FileNotFoundError: If file doesn't exist
159
199
  ValueError: If file is invalid or doesn't validate
@@ -161,16 +201,16 @@ class AgentEnvironmentsConfig(UtilsBaseModel):
161
201
  path = Path(file_path)
162
202
  if not path.exists():
163
203
  raise FileNotFoundError(f"environments.yaml not found: {file_path}")
164
-
204
+
165
205
  try:
166
- with open(path, 'r') as f:
206
+ with open(path, "r") as f:
167
207
  data = yaml.safe_load(f)
168
-
208
+
169
209
  if not data:
170
210
  raise ValueError("environments.yaml file is empty")
171
-
211
+
172
212
  return cls.model_validate(data)
173
-
213
+
174
214
  except yaml.YAMLError as e:
175
215
  raise ValueError(f"Invalid YAML format in {file_path}: {e}") from e
176
216
  except Exception as e:
@@ -179,18 +219,18 @@ class AgentEnvironmentsConfig(UtilsBaseModel):
179
219
 
180
220
  def load_environments_config_from_manifest_dir(manifest_dir: Path) -> AgentEnvironmentsConfig | None:
181
221
  """Helper function to load environments.yaml from same directory as manifest.yaml.
182
-
222
+
183
223
  Args:
184
224
  manifest_dir: Directory containing manifest.yaml
185
-
225
+
186
226
  Returns:
187
227
  AgentEnvironmentsConfig if environments.yaml exists, None otherwise
188
-
228
+
189
229
  Raises:
190
230
  ValueError: If environments.yaml exists but is invalid
191
231
  """
192
232
  environments_file = manifest_dir / "environments.yaml"
193
233
  if not environments_file.exists():
194
234
  return None
195
-
235
+
196
236
  return AgentEnvironmentsConfig.from_yaml(str(environments_file))