diff --git a/src/sk-agents/manual_test/validate_metadata.py b/src/sk-agents/manual_test/validate_metadata.py index c57711bff..bb6939e33 100644 --- a/src/sk-agents/manual_test/validate_metadata.py +++ b/src/sk-agents/manual_test/validate_metadata.py @@ -40,17 +40,20 @@ def print_header(title: str): + """Print a formatted section header.""" print(f"\n{'='*60}") print(f" {title}") print(f"{'='*60}") def print_response(data: dict): - print(f" Response JSON:") + """Print the JSON response data.""" + print(" Response JSON:") print(f" {json.dumps(data, indent=4)}") def check(label: str, condition: bool, actual=None, expected=None): + """Check a condition and print PASS/FAIL result.""" if condition: print(f" {PASS} {label}") else: @@ -98,11 +101,46 @@ def test_appv1_metadata(): print_response(data) results = [] - results.append(check("Status code is 200", response.status_code == 200, response.status_code, 200)) - results.append(check("agent_name is 'WeatherBot'", data["agent_name"] == "WeatherBot", data["agent_name"], "WeatherBot")) - results.append(check("description is 'A weather chat agent'", data["description"] == "A weather chat agent", data["description"], "A weather chat agent")) - results.append(check("model is 'gpt-4o'", data["model"] == "gpt-4o", data["model"], "gpt-4o")) - results.append(check("plugins contains 'WeatherPlugin'", data["plugins"] == ["WeatherPlugin"], data["plugins"], ["WeatherPlugin"])) + results.append( + check( + "Status code is 200", + response.status_code == 200, + response.status_code, + 200, + ) + ) + results.append( + check( + "agent_name is 'WeatherBot'", + data["agent_name"] == "WeatherBot", + data["agent_name"], + "WeatherBot", + ) + ) + results.append( + check( + "description is 'A weather chat agent'", + data["description"] == "A weather chat agent", + data["description"], + "A weather chat agent", + ) + ) + results.append( + check( + "model is 'gpt-4o'", + data["model"] == "gpt-4o", + data["model"], + "gpt-4o", + ) + ) + results.append( + check( + "plugins contains 'WeatherPlugin'", + data["plugins"] == ["WeatherPlugin"], + data["plugins"], + ["WeatherPlugin"], + ) + ) return all(results) @@ -143,11 +181,46 @@ def test_appv3_metadata(): print_response(data) results = [] - results.append(check("Status code is 200", response.status_code == 200, response.status_code, 200)) - results.append(check("agent_name is 'MathAgent'", data["agent_name"] == "MathAgent", data["agent_name"], "MathAgent")) - results.append(check("description is 'A math helper agent'", data["description"] == "A math helper agent", data["description"], "A math helper agent")) - results.append(check("model is 'gpt-4o-2024-05-13'", data["model"] == "gpt-4o-2024-05-13", data["model"], "gpt-4o-2024-05-13")) - results.append(check("plugins contains 'sensitive_plugin'", data["plugins"] == ["sensitive_plugin"], data["plugins"], ["sensitive_plugin"])) + results.append( + check( + "Status code is 200", + response.status_code == 200, + response.status_code, + 200, + ) + ) + results.append( + check( + "agent_name is 'MathAgent'", + data["agent_name"] == "MathAgent", + data["agent_name"], + "MathAgent", + ) + ) + results.append( + check( + "description is 'A math helper agent'", + data["description"] == "A math helper agent", + data["description"], + "A math helper agent", + ) + ) + results.append( + check( + "model is 'gpt-4o-2024-05-13'", + data["model"] == "gpt-4o-2024-05-13", + data["model"], + "gpt-4o-2024-05-13", + ) + ) + results.append( + check( + "plugins contains 'sensitive_plugin'", + data["plugins"] == ["sensitive_plugin"], + data["plugins"], + ["sensitive_plugin"], + ) + ) return all(results) @@ -185,9 +258,26 @@ def test_appv1_no_plugins(): results = [] results.append(check("Status code is 200", response.status_code == 200)) - results.append(check("agent_name is 'ChatBot'", data["agent_name"] == "ChatBot")) - results.append(check("model is 'gpt-4o-mini'", data["model"] == "gpt-4o-mini")) - results.append(check("plugins is None (no plugins)", data["plugins"] is None, data["plugins"], None)) + results.append( + check( + "agent_name is 'ChatBot'", + data["agent_name"] == "ChatBot", + ) + ) + results.append( + check( + "model is 'gpt-4o-mini'", + data["model"] == "gpt-4o-mini", + ) + ) + results.append( + check( + "plugins is None (no plugins)", + data["plugins"] is None, + data["plugins"], + None, + ) + ) return all(results) @@ -236,12 +326,14 @@ def test_appv3_with_metadata_description(): results = [] results.append(check("Status code is 200", response.status_code == 200)) - results.append(check( - "description uses metadata.description (not top-level)", - data["description"] == "A demonstration agent that showcases MCP integration", - data["description"], - "A demonstration agent that showcases MCP integration", - )) + results.append( + check( + "description uses metadata.description (not top-level)", + data["description"] == "A demonstration agent that showcases MCP integration", + data["description"], + "A demonstration agent that showcases MCP integration", + ) + ) return all(results) @@ -270,7 +362,14 @@ def test_response_schema(): print(f" Actual fields: {actual_fields}") results = [] - results.append(check("Response has exactly 4 expected fields", actual_fields == expected_fields, actual_fields, expected_fields)) + results.append( + check( + "Response has exactly 4 expected fields", + actual_fields == expected_fields, + actual_fields, + expected_fields, + ) + ) return all(results) @@ -291,11 +390,11 @@ def test_response_schema(): print_header("FINAL RESULT") if all_passed: print(f" {PASS} ALL TESTS PASSED — Ticket is resolved!") - print(f"\n The /metadata endpoint correctly returns:") - print(f" • agent_name — from config name/service_name") - print(f" • description — from metadata.description or top-level") - print(f" • model — from spec.agent.model") - print(f" • plugins — from spec.agent.plugins + remote_plugins + mcp_servers") + print("\n The /metadata endpoint correctly returns:") + print(" • agent_name — from config name/service_name") + print(" • description — from metadata.description or top-level") + print(" • model — from spec.agent.model") + print(" • plugins — from spec.agent.plugins + remote_plugins + mcp_servers") sys.exit(0) else: print(f" {FAIL} SOME TESTS FAILED — See above for details") diff --git a/src/sk-agents/pyproject.toml b/src/sk-agents/pyproject.toml index 6526a306c..df292a40f 100644 --- a/src/sk-agents/pyproject.toml +++ b/src/sk-agents/pyproject.toml @@ -67,6 +67,8 @@ dev = [ "mkdocs-material>=9.6.0", "mkdocstrings[python]>=0.28.0", "mkdocs-static-i18n>=1.3.0", + "black>=26.3.1", + "pylint>=4.0.5", ] [tool.ruff] @@ -91,6 +93,12 @@ isort = { combine-as-imports = true } [tool.ruff.lint.pydocstyle] convention = "google" +[tool.pylint."messages_control"] +disable = ["duplicate-code"] + +[tool.pylint.format] +max-line-length = 100 + [tool.mypy] strict = false disallow_incomplete_defs = false diff --git a/src/sk-agents/src/sk_agents/app.py b/src/sk-agents/src/sk_agents/app.py index 47a416dba..1ac2a32f0 100644 --- a/src/sk-agents/src/sk_agents/app.py +++ b/src/sk-agents/src/sk_agents/app.py @@ -41,7 +41,7 @@ class AppVersion(Enum): raise try: - (root_handler, api_version) = config.apiVersion.split("/") + root_handler, api_version = config.apiVersion.split("/") except ValueError: logger.exception("Invalid API version format") raise diff --git a/src/sk-agents/src/sk_agents/appv1.py b/src/sk-agents/src/sk_agents/appv1.py index f528e694f..3407e48b0 100644 --- a/src/sk-agents/src/sk_agents/appv1.py +++ b/src/sk-agents/src/sk_agents/appv1.py @@ -1,3 +1,5 @@ +"""AppV1 application runner for skagents/v1 API version.""" + import os from datetime import datetime from typing import Any @@ -18,9 +20,12 @@ from sk_agents.utils import initialize_plugin_loader -class AppV1: +class AppV1: # pylint: disable=too-few-public-methods + """Application runner for skagents/v1 API version.""" + @staticmethod def run(name: str, version: str, app_config: AppConfig, config: BaseConfig, app: FastAPI): + """Initialize and run the AppV1 application with routes and plugins.""" config_file = app_config.get(TA_SERVICE_CONFIG.env_name) agents_path = str(os.path.dirname(config_file)) diff --git a/src/sk-agents/src/sk_agents/appv3.py b/src/sk-agents/src/sk_agents/appv3.py index c83e74d26..64c2a25c2 100644 --- a/src/sk-agents/src/sk_agents/appv3.py +++ b/src/sk-agents/src/sk_agents/appv3.py @@ -34,8 +34,12 @@ def run(name, version, app_config, config, app): from sk_agents.utils import initialize_plugin_loader -class AppV3: +class AppV3: # pylint: disable=too-few-public-methods + """Application runner for tealagents/v1alpha1 API version.""" + class StateStores(Enum): + """Supported state store backends.""" + IN_MEMORY = "in-memory" REDIS = "redis" @@ -72,13 +76,14 @@ def _get_auth_storage_manager(app_config: AppConfig): @staticmethod def _get_mcp_discovery_manager(app_config: AppConfig): + # pylint: disable=import-outside-toplevel from sk_agents.mcp_discovery import DiscoveryManagerFactory discovery_factory = DiscoveryManagerFactory(app_config) return discovery_factory.get_discovery_manager() @staticmethod - def _get_auth_manager(app_config: AppConfig): + def _get_auth_manager(app_config: AppConfig): # pylint: disable=unused-argument # For initial implementation, use mock authentication # Will be extended in future for Entra ID return MockAuthenticationManager() @@ -103,6 +108,7 @@ def _create_kernel_builder(app_config: AppConfig, authorization: str): @staticmethod def run(name: str, version: str, app_config: AppConfig, config: BaseConfig, app: FastAPI): + """Initialize and run the AppV3 application with routes and plugins.""" if config.apiVersion != "tealagents/v1alpha1": raise ValueError( f"AppV3 only supports 'tealagents/v1alpha1' API version, got: {config.apiVersion}" diff --git a/src/sk-agents/src/sk_agents/auth/oauth_client.py b/src/sk-agents/src/sk_agents/auth/oauth_client.py index cd12768be..a4eeba83c 100644 --- a/src/sk-agents/src/sk_agents/auth/oauth_client.py +++ b/src/sk-agents/src/sk_agents/auth/oauth_client.py @@ -610,9 +610,9 @@ async def handle_callback( code=code, redirect_uri=server_config.oauth_redirect_uri, code_verifier=flow_state.verifier, - resource=flow_state.resource - if include_resource - else None, # Conditional per protocol version + resource=( + flow_state.resource if include_resource else None + ), # Conditional per protocol version client_id=server_config.oauth_client_id or client_name, client_secret=server_config.oauth_client_secret, requested_scopes=flow_state.scopes, # For scope validation diff --git a/src/sk-agents/src/sk_agents/mcp_client.py b/src/sk-agents/src/sk_agents/mcp_client.py index 93f73d5e2..d43afe359 100644 --- a/src/sk-agents/src/sk_agents/mcp_client.py +++ b/src/sk-agents/src/sk_agents/mcp_client.py @@ -451,9 +451,9 @@ def apply_trust_level_governance( logger.debug("Applying sandboxed server governance: elevated restrictions") return Governance( requires_hitl=True, # Force HITL for sandboxed servers - cost=base_governance.cost - if base_governance.cost != "low" - else "medium", # Elevate cost + cost=( + base_governance.cost if base_governance.cost != "low" else "medium" + ), # Elevate cost data_sensitivity=base_governance.data_sensitivity, ) else: # trusted @@ -521,13 +521,17 @@ def apply_governance_overrides( # Apply selective overrides - only override specified fields return Governance( - requires_hitl=override.requires_hitl - if override.requires_hitl is not None - else base_governance.requires_hitl, + requires_hitl=( + override.requires_hitl + if override.requires_hitl is not None + else base_governance.requires_hitl + ), cost=override.cost if override.cost is not None else base_governance.cost, - data_sensitivity=override.data_sensitivity - if override.data_sensitivity is not None - else base_governance.data_sensitivity, + data_sensitivity=( + override.data_sensitivity + if override.data_sensitivity is not None + else base_governance.data_sensitivity + ), ) @@ -719,9 +723,9 @@ async def resolve_server_auth_headers( refresh_request = RefreshTokenRequest( token_endpoint=token_endpoint, refresh_token=auth_data.refresh_token, - resource=resource_uri - if include_resource - else None, # Conditional per protocol version + resource=( + resource_uri if include_resource else None + ), # Conditional per protocol version client_id=server_config.oauth_client_id or app_config.get("TA_OAUTH_CLIENT_NAME"), client_secret=server_config.oauth_client_secret, diff --git a/src/sk-agents/src/sk_agents/mcp_plugin_registry.py b/src/sk-agents/src/sk_agents/mcp_plugin_registry.py index 9c52732f0..8228ee51d 100644 --- a/src/sk-agents/src/sk_agents/mcp_plugin_registry.py +++ b/src/sk-agents/src/sk_agents/mcp_plugin_registry.py @@ -54,13 +54,17 @@ def _apply_governance_overrides( override = overrides[tool_name] return Governance( - requires_hitl=override.requires_hitl - if override.requires_hitl is not None - else base_governance.requires_hitl, + requires_hitl=( + override.requires_hitl + if override.requires_hitl is not None + else base_governance.requires_hitl + ), cost=override.cost if override.cost is not None else base_governance.cost, - data_sensitivity=override.data_sensitivity - if override.data_sensitivity is not None - else base_governance.data_sensitivity, + data_sensitivity=( + override.data_sensitivity + if override.data_sensitivity is not None + else base_governance.data_sensitivity + ), ) @staticmethod diff --git a/src/sk-agents/src/sk_agents/persistence/in_memory_persistence_manager.py b/src/sk-agents/src/sk_agents/persistence/in_memory_persistence_manager.py index d2c08f2ca..2851a04e0 100644 --- a/src/sk-agents/src/sk_agents/persistence/in_memory_persistence_manager.py +++ b/src/sk-agents/src/sk_agents/persistence/in_memory_persistence_manager.py @@ -17,9 +17,9 @@ class InMemoryPersistenceManager(TaskPersistenceManager): def __init__(self): self.in_memory: dict[str, AgentTask] = {} - self.item_request_id_index: dict[ - str, set[str] - ] = {} # Maps request_id to set of task_ids that contain it + self.item_request_id_index: dict[str, set[str]] = ( + {} + ) # Maps request_id to set of task_ids that contain it logger.info("InMemoryPersistenceManager initialized.") self._lock = asyncio.Lock() diff --git a/src/sk-agents/src/sk_agents/plugin_catalog/local_plugin_catalog.py b/src/sk-agents/src/sk_agents/plugin_catalog/local_plugin_catalog.py index 7ab8b560f..2acf647e8 100644 --- a/src/sk-agents/src/sk_agents/plugin_catalog/local_plugin_catalog.py +++ b/src/sk-agents/src/sk_agents/plugin_catalog/local_plugin_catalog.py @@ -106,9 +106,7 @@ def _load_plugins(self) -> None: # Re-raise our custom exception raise except Exception as e: - raise PluginFileReadException( - message=""" + raise PluginFileReadException(message=""" Catalog encountered an error when attempting to read file - """ - ) from e + """) from e diff --git a/src/sk-agents/src/sk_agents/utility_routes.py b/src/sk-agents/src/sk_agents/utility_routes.py index 6f89d1f76..939586d20 100644 --- a/src/sk-agents/src/sk_agents/utility_routes.py +++ b/src/sk-agents/src/sk_agents/utility_routes.py @@ -1,3 +1,5 @@ +"""Utility routes for health checks, liveness, and agent metadata.""" + import logging from datetime import datetime from typing import Any @@ -54,7 +56,7 @@ def __init__(self, start_time: datetime | None = None): def get_health_routes( self, config: BaseConfig, - app_config: AppConfig, + app_config: AppConfig, # pylint: disable=unused-argument ) -> APIRouter: """ Get health check routes for the application. @@ -90,7 +92,7 @@ async def health_check() -> HealthStatus: uptime=uptime, ) except Exception as e: - logger.exception(f"Health check failed: {e}") + logger.exception("Health check failed: %s", e) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Service unhealthy" ) from e @@ -110,7 +112,7 @@ async def liveness_check() -> LivenessStatus: try: return LivenessStatus(alive=True, timestamp=datetime.now().isoformat()) except Exception as e: - logger.exception(f"Liveness check failed: {e}") + logger.exception("Liveness check failed: %s", e) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Service not alive" ) from e @@ -124,6 +126,47 @@ def _safe_get(obj, key, default=None): return obj.get(key, default) return getattr(obj, key, default) + @staticmethod + def _extract_description(config: BaseConfig) -> str | None: + """Extract description from config, preferring metadata.description.""" + if config.metadata is not None and config.metadata.description is not None: + return config.metadata.description + return config.description + + @staticmethod + def _extract_plugins_from_agent(agent, _get) -> list[str]: + """Extract plugin names from a single agent config.""" + plugins: list[str] = [] + agent_plugins = _get(agent, "plugins") + if agent_plugins: + plugins.extend(agent_plugins) + remote_plugins = _get(agent, "remote_plugins") + if remote_plugins: + plugins.extend(remote_plugins) + mcp_servers = _get(agent, "mcp_servers") + if mcp_servers: + for server in mcp_servers: + server_name = _get(server, "name") + if server_name: + plugins.append(f"mcp:{server_name}") + return plugins + + @staticmethod + def _extract_from_multi_agents(agents, plugins, _get) -> str | None: + """Extract model and plugins from a multi-agent spec. Returns combined model string.""" + models = [] + for ag in agents: + ag_model = _get(ag, "model") + if ag_model and ag_model not in models: + models.append(ag_model) + for p in _get(ag, "plugins") or []: + if p not in plugins: + plugins.append(p) + for p in _get(ag, "remote_plugins") or []: + if p not in plugins: + plugins.append(p) + return ", ".join(models) if models else None + @staticmethod def _extract_metadata(config: BaseConfig) -> AgentMetadata: """ @@ -140,66 +183,30 @@ def _extract_metadata(config: BaseConfig) -> AgentMetadata: try: _get = UtilityRoutes._safe_get agent_name = config.name or config.service_name - description = None + description = UtilityRoutes._extract_description(config) model = None plugins: list[str] = [] - # Get description from metadata if available, otherwise from top-level - if config.metadata is not None and config.metadata.description is not None: - description = config.metadata.description - elif config.description is not None: - description = config.description - - # Extract model and plugins from spec.agent (both skagents and tealagents) if config.spec is not None: spec = config.spec - # Handle single agent config (chat / tealagents) agent = _get(spec, "agent") if agent is not None: model = _get(agent, "model") - agent_plugins = _get(agent, "plugins") - if agent_plugins: - plugins.extend(agent_plugins) - remote_plugins = _get(agent, "remote_plugins") - if remote_plugins: - plugins.extend(remote_plugins) - mcp_servers = _get(agent, "mcp_servers") - if mcp_servers: - for server in mcp_servers: - server_name = _get(server, "name") - if server_name: - plugins.append(f"mcp:{server_name}") - - # Handle multi-agent config (sequential) + plugins = UtilityRoutes._extract_plugins_from_agent(agent, _get) + agents = _get(spec, "agents") if agents is not None: - models = [] - for ag in agents: - ag_model = _get(ag, "model") - if ag_model and ag_model not in models: - models.append(ag_model) - ag_plugins = _get(ag, "plugins") - if ag_plugins: - for p in ag_plugins: - if p not in plugins: - plugins.append(p) - ag_remote = _get(ag, "remote_plugins") - if ag_remote: - for p in ag_remote: - if p not in plugins: - plugins.append(p) - if models: - model = ", ".join(models) - - logger.info(f"Extracted metadata for agent: {agent_name}") + model = UtilityRoutes._extract_from_multi_agents(agents, plugins, _get) + + logger.info("Extracted metadata for agent: %s", agent_name) return AgentMetadata( agent_name=agent_name, description=description, model=model, plugins=plugins if plugins else None, ) - except Exception as e: - logger.exception(f"Failed to extract metadata from config: {e}") + except Exception as e: # pylint: disable=broad-exception-caught + logger.exception("Failed to extract metadata from config: %s", e) return AgentMetadata() def get_metadata_routes( @@ -222,7 +229,10 @@ def get_metadata_routes( "/metadata", response_model=AgentMetadata, summary="Agent metadata endpoint", - description="Returns metadata about the agent including name, description, model, and available plugins", + description=( + "Returns metadata about the agent including" + " name, description, model, and available plugins" + ), tags=["Metadata"], ) async def get_metadata() -> AgentMetadata: @@ -232,7 +242,7 @@ async def get_metadata() -> AgentMetadata: try: return metadata except Exception as e: - logger.exception(f"Metadata endpoint failed: {e}") + logger.exception("Metadata endpoint failed: %s", e) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve agent metadata", diff --git a/src/sk-agents/tests/tealagents/v1alpha1/agent/test_tealagents_v1alpha1_agent_handler.py b/src/sk-agents/tests/tealagents/v1alpha1/agent/test_tealagents_v1alpha1_agent_handler.py index a2fcc0584..df793cd50 100644 --- a/src/sk-agents/tests/tealagents/v1alpha1/agent/test_tealagents_v1alpha1_agent_handler.py +++ b/src/sk-agents/tests/tealagents/v1alpha1/agent/test_tealagents_v1alpha1_agent_handler.py @@ -94,7 +94,9 @@ def mock_app_config(): mock_config.get.side_effect = lambda key: { TA_PERSISTENCE_MODULE.env_name: TA_PERSISTENCE_MODULE.default_value, TA_PERSISTENCE_CLASS.env_name: TA_PERSISTENCE_CLASS.default_value, - }.get(key, MagicMock()) # Return MagicMock for other keys + }.get( + key, MagicMock() + ) # Return MagicMock for other keys return mock_config @@ -381,9 +383,9 @@ async def test_manage_agent_response_task( Test that _manage_agent_response_task correctly appends a new item and updates the agent task in state. """ - mocker.patch( - "sk_agents.tealagents.v1alpha1.agent.handler.datetime" - ).now.return_value = mock_date_time + mocker.patch("sk_agents.tealagents.v1alpha1.agent.handler.datetime").now.return_value = ( + mock_date_time + ) mocker.patch.object(teal_agents_handler.state, "update", new_callable=mocker.AsyncMock) diff --git a/src/sk-agents/tests/test_appv1.py b/src/sk-agents/tests/test_appv1.py index 79f2954df..f6a85e2cb 100644 --- a/src/sk-agents/tests/test_appv1.py +++ b/src/sk-agents/tests/test_appv1.py @@ -1,3 +1,5 @@ +"""Tests for AppV1 application runner.""" + import os from typing import Any from unittest.mock import MagicMock, patch diff --git a/src/sk-agents/tests/test_appv3.py b/src/sk-agents/tests/test_appv3.py index 62c62bdb4..b5f73b9d9 100644 --- a/src/sk-agents/tests/test_appv3.py +++ b/src/sk-agents/tests/test_appv3.py @@ -1,3 +1,5 @@ +"""Tests for AppV3 application runner.""" + from unittest.mock import MagicMock, patch import pytest diff --git a/src/sk-agents/tests/test_utility_routes.py b/src/sk-agents/tests/test_utility_routes.py index 53ca96c79..74735bf47 100644 --- a/src/sk-agents/tests/test_utility_routes.py +++ b/src/sk-agents/tests/test_utility_routes.py @@ -1,3 +1,5 @@ +"""Tests for utility routes including health, liveness, and metadata endpoints.""" + from unittest.mock import MagicMock, PropertyMock, patch from fastapi import FastAPI @@ -308,7 +310,7 @@ def test_extract_metadata_logs_info_on_success(self): with patch("sk_agents.utility_routes.logger") as mock_logger: UtilityRoutes._extract_metadata(config) mock_logger.info.assert_called_once_with( - "Extracted metadata for agent: log-test-agent" + "Extracted metadata for agent: %s", "log-test-agent" ) def test_extract_metadata_logs_exception_on_error(self): @@ -461,25 +463,30 @@ def test_agent_metadata_serialization(self): "plugins": ["P1"], } + class TestSafeGet: """Test UtilityRoutes._safe_get static method with dicts and objects.""" def test_safe_get_from_dict(self): + """Test _safe_get retrieves values from a dict.""" data = {"name": "agent1", "model": "gpt-4o"} assert UtilityRoutes._safe_get(data, "name") == "agent1" assert UtilityRoutes._safe_get(data, "model") == "gpt-4o" def test_safe_get_from_dict_missing_key(self): + """Test _safe_get returns default for missing dict keys.""" data = {"name": "agent1"} assert UtilityRoutes._safe_get(data, "missing") is None assert UtilityRoutes._safe_get(data, "missing", "fallback") == "fallback" def test_safe_get_from_object(self): + """Test _safe_get retrieves attributes from an object.""" obj = MagicMock() obj.name = "agent1" assert UtilityRoutes._safe_get(obj, "name") == "agent1" def test_safe_get_from_object_missing_attr(self): + """Test _safe_get returns None for missing object attributes.""" obj = MagicMock(spec=[]) assert UtilityRoutes._safe_get(obj, "missing") is None @@ -488,6 +495,7 @@ class TestExtractMetadataWithDictSpec: """Test _extract_metadata with dict-based specs (real YAML parsing scenario).""" def test_single_agent_dict_spec(self): + """Test extraction from a single-agent dict spec.""" config = BaseConfig( apiVersion="skagents/v1", name="ChatBot", @@ -509,6 +517,7 @@ def test_single_agent_dict_spec(self): assert metadata.plugins == ["PluginA", "PluginB", "RemotePlugin"] def test_multi_agent_dict_spec(self): + """Test extraction from a multi-agent dict spec.""" config = BaseConfig( apiVersion="skagents/v1", service_name="WeatherAgent", @@ -532,6 +541,7 @@ def test_multi_agent_dict_spec(self): assert metadata.plugins == ["WeatherPlugin"] def test_multi_agent_dict_spec_multiple_agents(self): + """Test extraction with multiple agents combines models and deduplicates plugins.""" config = BaseConfig( apiVersion="skagents/v1", name="MultiAgent", @@ -548,6 +558,7 @@ def test_multi_agent_dict_spec_multiple_agents(self): assert metadata.plugins == ["PluginA", "PluginB"] def test_dict_spec_with_mcp_servers(self): + """Test extraction includes MCP server names prefixed with 'mcp:'.""" config = BaseConfig( apiVersion="tealagents/v1alpha1", name="MCPAgent", @@ -569,6 +580,7 @@ def test_dict_spec_with_mcp_servers(self): assert metadata.plugins == ["LocalPlugin", "mcp:filesystem", "mcp:github"] def test_dict_spec_no_plugins(self): + """Test extraction returns None for plugins when none are configured.""" config = BaseConfig( apiVersion="skagents/v1", name="NoPluginAgent", @@ -580,6 +592,7 @@ def test_dict_spec_no_plugins(self): assert metadata.plugins is None def test_dict_spec_endpoint_integration(self): + """Test the /metadata endpoint returns correct data for dict-based specs.""" config = BaseConfig( apiVersion="skagents/v1", service_name="WeatherAgent", diff --git a/src/sk-agents/uv.lock b/src/sk-agents/uv.lock index e7c4cf4e8..dfb625f26 100644 --- a/src/sk-agents/uv.lock +++ b/src/sk-agents/uv.lock @@ -182,6 +182,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -375,6 +384,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + [[package]] name = "boto3" version = "1.42.53" @@ -644,6 +680,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, ] +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -1180,6 +1225,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + [[package]] name = "jaraco-classes" version = "3.4.0" @@ -1473,6 +1527,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + [[package]] name = "mcp" version = "1.26.0" @@ -2362,6 +2425,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, ] +[[package]] +name = "pylint" +version = "4.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, +] + [[package]] name = "pymdown-extensions" version = "10.21" @@ -2511,6 +2592,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -2961,6 +3061,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "black" }, { name = "coverage" }, { name = "hatch" }, { name = "hatchling" }, @@ -2969,6 +3070,7 @@ dev = [ { name = "mkdocs-static-i18n" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, + { name = "pylint" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -2998,6 +3100,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "black", specifier = ">=26.3.1" }, { name = "coverage" }, { name = "hatch" }, { name = "hatchling" }, @@ -3006,6 +3109,7 @@ dev = [ { name = "mkdocs-static-i18n", specifier = ">=1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.28.0" }, { name = "mypy" }, + { name = "pylint", specifier = ">=4.0.5" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" },