apex-core 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.
- apex_core/__init__.py +23 -0
- apex_core/agents/__init__.py +13 -0
- apex_core/agents/agent_config.py +293 -0
- apex_core/agents/agent_factory.py +240 -0
- apex_core/agents/domain_agent.py +109 -0
- apex_core/agents/interaction_logger.py +429 -0
- apex_core/agents/llm_factory.py +272 -0
- apex_core/agents/swarm_orchestrator.py +419 -0
- apex_core/agents/tool_wrapper.py +222 -0
- apex_core/config/agents/compute.yaml +72 -0
- apex_core/config/agents/containers.yaml +58 -0
- apex_core/config/agents/identity.yaml +63 -0
- apex_core/config/agents/init.yaml +76 -0
- apex_core/config/agents/mlops.yaml +70 -0
- apex_core/config/agents/networking.yaml +78 -0
- apex_core/config/agents/observability.yaml +59 -0
- apex_core/config/agents/platform.yaml +60 -0
- apex_core/config/aws_acm.yaml +131 -0
- apex_core/config/aws_alb.yaml +663 -0
- apex_core/config/aws_apigateway.yaml +361 -0
- apex_core/config/aws_cloudformation.yaml +263 -0
- apex_core/config/aws_cloudwatch_logs.yaml +239 -0
- apex_core/config/aws_ec2.yaml +63 -0
- apex_core/config/aws_ecr.yaml +367 -0
- apex_core/config/aws_ecs.yaml +701 -0
- apex_core/config/aws_ecs_app_workflow.yaml +223 -0
- apex_core/config/aws_efs.yaml +219 -0
- apex_core/config/aws_iam.yaml +339 -0
- apex_core/config/aws_nlb.yaml +318 -0
- apex_core/config/aws_operations.yaml +169 -0
- apex_core/config/aws_route53.yaml +183 -0
- apex_core/config/aws_s3.yaml +271 -0
- apex_core/config/aws_secrets_manager.yaml +237 -0
- apex_core/config/aws_service_discovery.yaml +200 -0
- apex_core/config/aws_sts.yaml +107 -0
- apex_core/config/aws_vpc.yaml +860 -0
- apex_core/config/aws_vpc_link.yaml +139 -0
- apex_core/config/aws_vpc_stack_workflow.yaml +186 -0
- apex_core/config/ci_runs.yaml +179 -0
- apex_core/config/code_quality.yaml +178 -0
- apex_core/config/databricks_operations.yaml +171 -0
- apex_core/config/docker_operations.yaml +366 -0
- apex_core/config/gcp_artifact_registry.yaml +382 -0
- apex_core/config/gcp_bigquery.yaml +186 -0
- apex_core/config/gcp_cloud_run.yaml +757 -0
- apex_core/config/gcp_cloud_sql.yaml +292 -0
- apex_core/config/gcp_firebase.yaml +635 -0
- apex_core/config/gcp_iam.yaml +520 -0
- apex_core/config/gcp_identity_platform.yaml +163 -0
- apex_core/config/gcp_inventory.yaml +141 -0
- apex_core/config/gcp_networking.yaml +553 -0
- apex_core/config/gcp_observability.yaml +107 -0
- apex_core/config/gcp_org_policy.yaml +146 -0
- apex_core/config/gcp_project.yaml +316 -0
- apex_core/config/gcp_pubsub.yaml +308 -0
- apex_core/config/gcp_redis.yaml +172 -0
- apex_core/config/gcp_secret_manager.yaml +304 -0
- apex_core/config/gcp_storage.yaml +309 -0
- apex_core/config/github_repo.yaml +773 -0
- apex_core/config/health_check.yaml +147 -0
- apex_core/config/iam/README.md +128 -0
- apex_core/config/iam/iam-admin-policy.json +293 -0
- apex_core/config/mlflow_operations.yaml +348 -0
- apex_core/config/penpot.yaml +321 -0
- apex_core/config/platform_control.yaml +64 -0
- apex_core/config/platform_workflows.yaml +314 -0
- apex_core/config/project_setup.yaml +179 -0
- apex_core/config/projects.yaml +99 -0
- apex_core/config/release_ops.yaml +163 -0
- apex_core/config/security.yaml +164 -0
- apex_core/config/skills.yaml +98 -0
- apex_core/config/templates/config_template.yaml +142 -0
- apex_core/config/templates/vpc_with_internet_access.yaml +91 -0
- apex_core/config/terraform_operations.yaml +302 -0
- apex_core/config/test_runner.yaml +253 -0
- apex_core/config/validation.yaml +168 -0
- apex_core/config/workflow_engine.yaml +156 -0
- apex_core/config/workflow_planning.yaml +91 -0
- apex_core/config/workflow_planning_resource_server.yaml +136 -0
- apex_core/core/__init__.py +12 -0
- apex_core/core/apex_settings.py +701 -0
- apex_core/core/capability_digest.py +56 -0
- apex_core/core/capability_resolver.py +815 -0
- apex_core/core/capability_shadows.py +195 -0
- apex_core/core/capability_sync.py +534 -0
- apex_core/core/diagnostics.py +49 -0
- apex_core/core/dsl_engine.py +630 -0
- apex_core/core/dsl_models.py +418 -0
- apex_core/core/execution_nodes.py +125 -0
- apex_core/core/flow_loader.py +165 -0
- apex_core/core/flow_models.py +279 -0
- apex_core/core/instance_manager.py +604 -0
- apex_core/core/langgraph_compiler.py +1452 -0
- apex_core/core/mcp_integration.py +245 -0
- apex_core/core/mode_manager.py +483 -0
- apex_core/core/operator_abstraction.py +326 -0
- apex_core/core/permission_models.py +44 -0
- apex_core/core/plan_store.py +187 -0
- apex_core/core/platform.py +608 -0
- apex_core/core/platform_config.py +292 -0
- apex_core/core/platform_models.py +529 -0
- apex_core/core/provenance.py +495 -0
- apex_core/core/requirements_checker.py +284 -0
- apex_core/core/resource_graph.py +258 -0
- apex_core/core/response_models.py +176 -0
- apex_core/core/review_passes.py +126 -0
- apex_core/core/server_factory.py +144 -0
- apex_core/core/skills_placement.py +237 -0
- apex_core/core/state_lifecycle.py +194 -0
- apex_core/core/validators/__init__.py +22 -0
- apex_core/core/validators/flow_validator.py +217 -0
- apex_core/core/validators/resource_server_validator.py +203 -0
- apex_core/core/validators/workflow_validator.py +127 -0
- apex_core/core/workflow_config.py +64 -0
- apex_core/core/workflow_planning_server.py +249 -0
- apex_core/core/workflow_resolver.py +213 -0
- apex_core/core/workflow_status.py +263 -0
- apex_core/llm/__init__.py +21 -0
- apex_core/llm/model_providers.py +451 -0
- apex_core/middleware/__init__.py +19 -0
- apex_core/middleware/audit_middleware.py +102 -0
- apex_core/middleware/execution_mode_middleware.py +47 -0
- apex_core/middleware/otel_export.py +335 -0
- apex_core/middleware/permission_middleware.py +65 -0
- apex_core/middleware/provenance_middleware.py +108 -0
- apex_core/middleware/tag_injection_middleware.py +156 -0
- apex_core/providers/__init__.py +89 -0
- apex_core/providers/aws/__init__.py +73 -0
- apex_core/providers/aws/aws_acm.py +156 -0
- apex_core/providers/aws/aws_alb.py +576 -0
- apex_core/providers/aws/aws_apigateway.py +418 -0
- apex_core/providers/aws/aws_base.py +84 -0
- apex_core/providers/aws/aws_cloudformation.py +328 -0
- apex_core/providers/aws/aws_cloudwatch_logs.py +297 -0
- apex_core/providers/aws/aws_ec2.py +110 -0
- apex_core/providers/aws/aws_ecr.py +387 -0
- apex_core/providers/aws/aws_ecs.py +627 -0
- apex_core/providers/aws/aws_efs.py +332 -0
- apex_core/providers/aws/aws_iam.py +308 -0
- apex_core/providers/aws/aws_nlb.py +338 -0
- apex_core/providers/aws/aws_response_models.py +161 -0
- apex_core/providers/aws/aws_route53.py +209 -0
- apex_core/providers/aws/aws_s3.py +281 -0
- apex_core/providers/aws/aws_secrets_manager.py +246 -0
- apex_core/providers/aws/aws_service_discovery.py +273 -0
- apex_core/providers/aws/aws_sts.py +122 -0
- apex_core/providers/aws/aws_vpc.py +1061 -0
- apex_core/providers/aws/aws_vpc_link.py +134 -0
- apex_core/providers/databricks/__init__.py +5 -0
- apex_core/providers/databricks/databricks_resource_server.py +381 -0
- apex_core/providers/devtools/__init__.py +22 -0
- apex_core/providers/devtools/ci_runs_resource_server.py +446 -0
- apex_core/providers/devtools/code_quality_resource_server.py +190 -0
- apex_core/providers/devtools/health_check_resource_server.py +203 -0
- apex_core/providers/devtools/release_ops_resource_server.py +342 -0
- apex_core/providers/devtools/test_runner_resource_server.py +273 -0
- apex_core/providers/devtools/validation_resource_server.py +256 -0
- apex_core/providers/docker/__init__.py +5 -0
- apex_core/providers/docker/docker_resource_server.py +696 -0
- apex_core/providers/gcp/__init__.py +80 -0
- apex_core/providers/gcp/gcp_artifact_registry.py +703 -0
- apex_core/providers/gcp/gcp_base.py +284 -0
- apex_core/providers/gcp/gcp_bigquery.py +383 -0
- apex_core/providers/gcp/gcp_cloud_run.py +1287 -0
- apex_core/providers/gcp/gcp_cloud_sql.py +447 -0
- apex_core/providers/gcp/gcp_firebase.py +1363 -0
- apex_core/providers/gcp/gcp_iam.py +901 -0
- apex_core/providers/gcp/gcp_identity_platform.py +450 -0
- apex_core/providers/gcp/gcp_inventory.py +840 -0
- apex_core/providers/gcp/gcp_networking.py +1057 -0
- apex_core/providers/gcp/gcp_observability.py +197 -0
- apex_core/providers/gcp/gcp_org_policy.py +294 -0
- apex_core/providers/gcp/gcp_project.py +571 -0
- apex_core/providers/gcp/gcp_pubsub.py +368 -0
- apex_core/providers/gcp/gcp_redis.py +268 -0
- apex_core/providers/gcp/gcp_secret_manager.py +528 -0
- apex_core/providers/gcp/gcp_security_validator.py +262 -0
- apex_core/providers/gcp/gcp_storage.py +525 -0
- apex_core/providers/github/__init__.py +48 -0
- apex_core/providers/github/github_base.py +169 -0
- apex_core/providers/github/github_repo.py +1356 -0
- apex_core/providers/internal/__init__.py +5 -0
- apex_core/providers/internal/platform_control.py +269 -0
- apex_core/providers/internal/project_setup_resource_server.py +991 -0
- apex_core/providers/internal/projects_resource_server.py +389 -0
- apex_core/providers/internal/skills_resource_server.py +409 -0
- apex_core/providers/internal/templates/aws-data-pipeline/AGENT.md +50 -0
- apex_core/providers/internal/templates/aws-data-pipeline/CONSTITUTION.md +25 -0
- apex_core/providers/internal/templates/aws-full-stack/AGENT.md +49 -0
- apex_core/providers/internal/templates/aws-full-stack/CONSTITUTION.md +25 -0
- apex_core/providers/internal/templates/aws-infrastructure/AGENT.md +47 -0
- apex_core/providers/internal/templates/aws-infrastructure/CONSTITUTION.md +25 -0
- apex_core/providers/internal/templates/aws-microservice/AGENT.md +61 -0
- apex_core/providers/internal/templates/aws-microservice/CONSTITUTION.md +25 -0
- apex_core/providers/internal/templates/aws-ml-platform/AGENT.md +47 -0
- apex_core/providers/internal/templates/aws-ml-platform/CONSTITUTION.md +25 -0
- apex_core/providers/internal/templates/common/AGENT.md +29 -0
- apex_core/providers/internal/templates/databricks-ml/AGENT.md +44 -0
- apex_core/providers/internal/templates/databricks-ml/CONSTITUTION.md +25 -0
- apex_core/providers/internal/templates/docker-local/AGENT.md +50 -0
- apex_core/providers/internal/templates/docker-local/CONSTITUTION.md +23 -0
- apex_core/providers/internal/workflow_engine.py +896 -0
- apex_core/providers/internal/workflow_planning_resource_server.py +724 -0
- apex_core/providers/mlflow/__init__.py +5 -0
- apex_core/providers/mlflow/mlflow_resource_server.py +596 -0
- apex_core/providers/penpot/__init__.py +49 -0
- apex_core/providers/penpot/penpot_base.py +354 -0
- apex_core/providers/penpot/penpot_geometry.py +204 -0
- apex_core/providers/penpot/penpot_operations.py +512 -0
- apex_core/providers/security/__init__.py +5 -0
- apex_core/providers/security/gitleaks.toml +81 -0
- apex_core/providers/security/security_scan_resource_server.py +461 -0
- apex_core/providers/terraform/__init__.py +5 -0
- apex_core/providers/terraform/terraform_resource_server.py +836 -0
- apex_core/providers/workflows/__init__.py +38 -0
- apex_core/skills/ci/SKILL.md +96 -0
- apex_core/skills/ci-triage/SKILL.md +88 -0
- apex_core/skills/containers/SKILL.md +152 -0
- apex_core/skills/data-quality/SKILL.md +140 -0
- apex_core/skills/gcp/SKILL.md +204 -0
- apex_core/skills/iac/SKILL.md +139 -0
- apex_core/skills/observability/SKILL.md +154 -0
- apex_core/skills/security/SKILL.md +130 -0
- apex_core/tools/__init__.py +10 -0
- apex_core/tools/apex_capabilities.py +280 -0
- apex_core/tools/apex_chat.py +219 -0
- apex_core/tools/apex_cli.py +511 -0
- apex_core/tools/apex_flows.py +162 -0
- apex_core/tools/apex_llm_models.py +314 -0
- apex_core/tools/apex_mcp_server.py +54 -0
- apex_core/tools/apex_plan.py +202 -0
- apex_core/tools/apex_state.py +1635 -0
- apex_core/tools/apex_ui.py +73 -0
- apex_core/tools/apex_validate.py +360 -0
- apex_core/tools/apex_workflows.py +294 -0
- apex_core/tools/cli_generator_agent.py +335 -0
- apex_core/tools/provenance_hooks.py +114 -0
- apex_core/tools/resource_mapper.py +1192 -0
- apex_core/tools/streamlit/__init__.py +0 -0
- apex_core/tools/streamlit/_helpers.py +131 -0
- apex_core/tools/streamlit/chat_app.py +114 -0
- apex_core/tools/streamlit/dashboard_app.py +165 -0
- apex_core/tools/streamlit/portal/__init__.py +1 -0
- apex_core/tools/streamlit/portal/audit_store.py +289 -0
- apex_core/tools/streamlit/portal/auth.py +154 -0
- apex_core/tools/streamlit/portal/catalog_store.py +340 -0
- apex_core/tools/streamlit/portal/components/__init__.py +0 -0
- apex_core/tools/streamlit/portal/components/execution_runner.py +194 -0
- apex_core/tools/streamlit/portal/components/param_form.py +157 -0
- apex_core/tools/streamlit/portal/components/workflow_dag.py +108 -0
- apex_core/tools/streamlit/portal/db.py +121 -0
- apex_core/tools/streamlit/portal/migrations/__init__.py +0 -0
- apex_core/tools/streamlit/portal/migrations/alembic.ini +37 -0
- apex_core/tools/streamlit/portal/migrations/env.py +63 -0
- apex_core/tools/streamlit/portal/migrations/versions/0001_create_executions_table.py +46 -0
- apex_core/tools/streamlit/portal/pages/__init__.py +0 -0
- apex_core/tools/streamlit/portal/pages/audit.py +122 -0
- apex_core/tools/streamlit/portal/pages/author.py +286 -0
- apex_core/tools/streamlit/portal/pages/catalog.py +154 -0
- apex_core/tools/streamlit/portal/pages/history.py +98 -0
- apex_core/tools/streamlit/portal/pages/metrics.py +107 -0
- apex_core/tools/streamlit/portal/pages/registry.py +134 -0
- apex_core/tools/streamlit/portal/portal_app.py +154 -0
- apex_core/tools/toolnode_agent.py +444 -0
- apex_core/workflows/apex/apex-release.yml +150 -0
- apex_core/workflows/aws/deploy_ecs_service_pipeline.yaml +131 -0
- apex_core/workflows/aws/deploy_vpc_stack.yaml +87 -0
- apex_core/workflows/aws/full_stack_deployment.yaml +62 -0
- apex_core/workflows/ci/deploy-pipeline.yml +118 -0
- apex_core/workflows/ci/full-pipeline.yml +108 -0
- apex_core/workflows/ci/gcp-deploy-pipeline.yml +156 -0
- apex_core/workflows/ci/terraform-deploy-pipeline.yml +73 -0
- apex_core/workflows/examples/deploy-test-ui-complete.dev.config.yml +22 -0
- apex_core/workflows/examples/deploy-test-ui-complete.prod.config.yml +23 -0
- apex_core/workflows/examples/deploy-test-ui-complete.yml +402 -0
- apex_core/workflows/examples/internet-facing-alb.yml +268 -0
- apex_core/workflows/gcp/apex-ci-setup.yml +138 -0
- apex_core/workflows/gcp/artifact-registry-push.yml +68 -0
- apex_core/workflows/gcp/cloud-run-deploy-image.yml +184 -0
- apex_core/workflows/gcp/cloud-run-deploy.yml +111 -0
- apex_core/workflows/gcp/cloud-run-zero-downtime.yml +110 -0
- apex_core/workflows/gcp/firebase-hosting-deploy.yml +49 -0
- apex_core/workflows/gcp/gcp-admin-bootstrap.yml +89 -0
- apex_core/workflows/gcp/gcp-database-setup.yml +145 -0
- apex_core/workflows/gcp/gcp-deploy-sa-wif-bind.yml +119 -0
- apex_core/workflows/gcp/gcp-firebase-init.yml +78 -0
- apex_core/workflows/gcp/gcp-org-bootstrap.yml +125 -0
- apex_core/workflows/gcp/gcp-product-env-bootstrap.yml +238 -0
- apex_core/workflows/gcp/gcp-project-bootstrap.yml +101 -0
- apex_core/workflows/gcp/gcp-secrets-provision.yml +46 -0
- apex_core/workflows/gcp/gcp-service-account-setup.yml +61 -0
- apex_core/workflows/gcp/gcp-storage-setup.yml +67 -0
- apex_core/workflows/gcp/gcp-wif-org-provider.yml +106 -0
- apex_core/workflows/gcp/gcp-wif-setup.yml +123 -0
- apex_core/workflows/gcp/publish-python-package.yml +100 -0
- apex_core/workflows/generated/deployment/deploy-apex-demo-api-deployment.yaml +244 -0
- apex_core/workflows/generated/infrastructure/deploy-apex-demo-api-infrastructure.yaml +364 -0
- apex_core/workflows/github/design-pr-merge.yml +53 -0
- apex_core/workflows/github/github-repo-bootstrap.yml +93 -0
- apex_core/workflows/modules/alb-with-target-group.yml +136 -0
- apex_core/workflows/modules/ci-quality-gate-python.yml +75 -0
- apex_core/workflows/modules/ci-quality-gate-typescript.yml +85 -0
- apex_core/workflows/modules/create-networking.yml +95 -0
- apex_core/workflows/modules/create-secret.yml +61 -0
- apex_core/workflows/modules/deploy-container-registry.yml +75 -0
- apex_core/workflows/modules/deploy-container-service.yml +97 -0
- apex_core/workflows/modules/deploy-static-site.yml +80 -0
- apex_core/workflows/modules/docker-build-and-push.yml +55 -0
- apex_core/workflows/modules/ecr-repository.yml +70 -0
- apex_core/workflows/modules/ecs-fargate-cluster.yml +45 -0
- apex_core/workflows/modules/ecs-service-with-alb.yml +244 -0
- apex_core/workflows/modules/health-check-services.yml +49 -0
- apex_core/workflows/modules/run-e2e-tests.yml +52 -0
- apex_core/workflows/modules/run-linting.yml +51 -0
- apex_core/workflows/modules/run-type-check.yml +43 -0
- apex_core/workflows/modules/run-unit-tests.yml +55 -0
- apex_core/workflows/modules/simple-test.yml +26 -0
- apex_core/workflows/modules/sync-secrets-to-gcp.yml +224 -0
- apex_core/workflows/modules/terraform-capture-outputs.yml +70 -0
- apex_core/workflows/modules/terraform-import-resources.yml +96 -0
- apex_core/workflows/modules/terraform-init-plan-apply.yml +48 -0
- apex_core/workflows/modules/terraform-validate-only.yml +82 -0
- apex_core/workflows/modules/terraform-workspace-deploy.yml +58 -0
- apex_core/workflows/modules/validate-deployment.yml +75 -0
- apex_core/workflows/modules/validate-infrastructure.yml +66 -0
- apex_core/workflows/modules/vpc-with-subnets.yml +158 -0
- apex_core/workflows/ops/cleanup_vpc.yml +46 -0
- apex_core/workflows/platform/simple_web_stack.yaml +75 -0
- apex_core/workflows/platform/vpc_internet_access.yaml +277 -0
- apex_core/workflows/platform/web_app_stack.yaml +123 -0
- apex_core-0.9.0.dist-info/METADATA +361 -0
- apex_core-0.9.0.dist-info/RECORD +335 -0
- apex_core-0.9.0.dist-info/WHEEL +4 -0
- apex_core-0.9.0.dist-info/entry_points.txt +3 -0
- apex_core-0.9.0.dist-info/licenses/LICENSE +124 -0
apex_core/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
APEX Platform - Multi-Cloud Infrastructure Orchestration Library
|
|
4
|
+
|
|
5
|
+
A provider-agnostic platform for orchestrating cloud infrastructure operations
|
|
6
|
+
with state management, execution planning, and conditional workflows.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- Multi-provider support (AWS, Azure, GCP, Kubernetes)
|
|
10
|
+
- State-aware execution with variable interpolation
|
|
11
|
+
- Execution planning with validation and rollback
|
|
12
|
+
- Dynamic class loading from YAML configuration
|
|
13
|
+
- MCP (Model Context Protocol) integration
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .core.platform import APEXPlatform
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
__author__ = "APEX Platform Team"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"APEXPlatform"
|
|
23
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
APEX Platform Agents
|
|
3
|
+
|
|
4
|
+
Config-driven, factory-based multi-domain agent system built on LangGraph Swarm.
|
|
5
|
+
|
|
6
|
+
Architecture:
|
|
7
|
+
SwarmOrchestrator — routes queries to domain agents, manages multi-turn state
|
|
8
|
+
AgentFactory — creates domain agents from YAML config definitions
|
|
9
|
+
AgentConfigLoader — discovers and validates agent YAML configs
|
|
10
|
+
DomainAgent — wraps a compiled LangGraph ReAct agent for one domain
|
|
11
|
+
tool_wrapper — builds middleware-compliant LangChain StructuredTools
|
|
12
|
+
InteractionLogger — logs routing decisions + tool calls to interactions.jsonl
|
|
13
|
+
"""
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Agent configuration models and config context detection.
|
|
4
|
+
|
|
5
|
+
Contains:
|
|
6
|
+
ConfigContext — project vs global context detection
|
|
7
|
+
AgentDomainConfig — Pydantic model for agent YAML configs
|
|
8
|
+
ModelConfig — nested LLM model configuration
|
|
9
|
+
QueryExample — training/test example in agent config
|
|
10
|
+
AgentConfigLoader — discovers and validates agent YAML configs
|
|
11
|
+
AgentConfigError — raised on config validation failures
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
from pydantic import BaseModel, Field, field_validator
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Config Context
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ConfigContext:
|
|
34
|
+
"""Determines which configs/skills/tools are available based on context.
|
|
35
|
+
|
|
36
|
+
Project context: .apex/settings.yaml exists in cwd or a parent directory.
|
|
37
|
+
Global context: no project config found; uses ~/.apex/ defaults.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
mode: str # "project" | "global"
|
|
41
|
+
project_dir: Optional[Path] # Project root (where .apex/ lives)
|
|
42
|
+
global_dir: Path # Always ~/.apex/
|
|
43
|
+
allowed_skills: Optional[List[str]] = field(default=None) # None = all
|
|
44
|
+
allowed_agents: Optional[List[str]] = field(default=None) # None = all
|
|
45
|
+
skills_repo: Optional[str] = field(default=None)
|
|
46
|
+
projects_repo: Optional[str] = field(default=None)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def detect(cls, cwd: Path = None) -> "ConfigContext":
|
|
50
|
+
"""Auto-detect context by walking up from cwd looking for .apex/settings.yaml.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
cwd: Starting directory. Defaults to Path.cwd().
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
ConfigContext with mode="project" if found, else mode="global".
|
|
57
|
+
"""
|
|
58
|
+
cwd = cwd or Path.cwd()
|
|
59
|
+
global_dir = Path.home() / ".apex"
|
|
60
|
+
|
|
61
|
+
# Walk up the directory tree looking for .apex/settings.yaml
|
|
62
|
+
current = cwd
|
|
63
|
+
while True:
|
|
64
|
+
candidate = current / ".apex" / "settings.yaml"
|
|
65
|
+
if candidate.exists():
|
|
66
|
+
# Found project context — load project-level overrides
|
|
67
|
+
allowed_skills = None
|
|
68
|
+
allowed_agents = None
|
|
69
|
+
skills_repo = None
|
|
70
|
+
projects_repo = None
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
with open(candidate) as f:
|
|
74
|
+
data = yaml.safe_load(f) or {}
|
|
75
|
+
agents_cfg = data.get("agents", {})
|
|
76
|
+
skills_cfg = data.get("skills", {})
|
|
77
|
+
allowed_skills = agents_cfg.get("allowed_skills") or skills_cfg.get("allowed_skills")
|
|
78
|
+
allowed_agents = agents_cfg.get("allowed_agents")
|
|
79
|
+
skills_repo = skills_cfg.get("skills_repo")
|
|
80
|
+
projects_repo = skills_cfg.get("projects_repo")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.warning("Failed to read project settings at %s: %s", candidate, e)
|
|
83
|
+
|
|
84
|
+
return cls(
|
|
85
|
+
mode="project",
|
|
86
|
+
project_dir=current,
|
|
87
|
+
global_dir=global_dir,
|
|
88
|
+
allowed_skills=allowed_skills,
|
|
89
|
+
allowed_agents=allowed_agents,
|
|
90
|
+
skills_repo=skills_repo,
|
|
91
|
+
projects_repo=projects_repo,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
parent = current.parent
|
|
95
|
+
if parent == current:
|
|
96
|
+
break # Reached filesystem root
|
|
97
|
+
current = parent
|
|
98
|
+
|
|
99
|
+
# No project config found — global context
|
|
100
|
+
return cls(
|
|
101
|
+
mode="global",
|
|
102
|
+
project_dir=None,
|
|
103
|
+
global_dir=global_dir,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def settings_path(self) -> Path:
|
|
108
|
+
"""Return the active settings.yaml path."""
|
|
109
|
+
if self.project_dir:
|
|
110
|
+
return self.project_dir / ".apex" / "settings.yaml"
|
|
111
|
+
return self.global_dir / "settings.yaml"
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def skills_dir(self) -> Path:
|
|
115
|
+
"""Return ~/.apex/skills/ directory."""
|
|
116
|
+
return self.global_dir / "skills"
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def conversations_db(self) -> Path:
|
|
120
|
+
"""Return path to shared conversations database."""
|
|
121
|
+
return self.global_dir / "conversations.db"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Pydantic models for agent YAML configs
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class QueryExample(BaseModel):
|
|
130
|
+
"""A single training/test example embedded in the agent config."""
|
|
131
|
+
|
|
132
|
+
query: str = Field(description="Natural language user query")
|
|
133
|
+
tool: str = Field(description="Expected tool name (e.g., 'create_vpc')")
|
|
134
|
+
server: str = Field(description="Expected server name (e.g., 'aws_vpc')")
|
|
135
|
+
parameters: Dict[str, Any] = Field(default_factory=dict, description="Expected tool parameters")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AgentDomainConfig(BaseModel):
|
|
139
|
+
"""Configuration for a domain agent, loaded from YAML.
|
|
140
|
+
|
|
141
|
+
One YAML file per domain in config/agents/*.yaml.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
name: str = Field(description="Unique domain identifier (e.g., 'infrastructure')")
|
|
145
|
+
description: str = Field(description="Domain description — used for FAISS domain search")
|
|
146
|
+
skill: Optional[str] = Field(default=None, description="SKILL.md name reference")
|
|
147
|
+
memory_mode: str = Field(default="persistent", description="'persistent' or 'stateless'")
|
|
148
|
+
resource_servers: List[str] = Field(description="ResourceServer names (e.g., ['aws_vpc', 'aws_subnet'])")
|
|
149
|
+
handoff_targets: List[str] = Field(default_factory=list, description="Sibling domain names for routing")
|
|
150
|
+
routing_keywords: List[str] = Field(default_factory=list, description="Keywords for fast-path routing")
|
|
151
|
+
system_prompt: Optional[str] = Field(default=None, description="Custom system prompt (overrides skill)")
|
|
152
|
+
examples: List[QueryExample] = Field(default_factory=list, description="Baseline training/test examples")
|
|
153
|
+
|
|
154
|
+
@field_validator("memory_mode")
|
|
155
|
+
@classmethod
|
|
156
|
+
def validate_memory_mode(cls, v: str) -> str:
|
|
157
|
+
allowed = {"persistent", "stateless"}
|
|
158
|
+
if v not in allowed:
|
|
159
|
+
raise ValueError(f"memory_mode must be one of {allowed}, got '{v}'")
|
|
160
|
+
return v
|
|
161
|
+
|
|
162
|
+
@field_validator("resource_servers")
|
|
163
|
+
@classmethod
|
|
164
|
+
def validate_resource_servers_not_empty(cls, v: List[str]) -> List[str]:
|
|
165
|
+
if not v:
|
|
166
|
+
raise ValueError("resource_servers must contain at least one server name")
|
|
167
|
+
return v
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Config loading error
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AgentConfigError(Exception):
|
|
176
|
+
"""Raised when agent config is invalid or cannot be loaded."""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# AgentConfigLoader
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class AgentConfigLoader:
|
|
185
|
+
"""Discovers and validates agent domain configs from YAML files.
|
|
186
|
+
|
|
187
|
+
Loads all YAML files from config_dir matching *.yaml and parses them
|
|
188
|
+
into AgentDomainConfig instances.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, config_dir: Path = None):
|
|
192
|
+
"""
|
|
193
|
+
Args:
|
|
194
|
+
config_dir: Path to agents config directory.
|
|
195
|
+
Default: auto-discovers config/agents/ relative to this file.
|
|
196
|
+
"""
|
|
197
|
+
if config_dir is None:
|
|
198
|
+
# Auto-discover: walk up from this file to find config/agents/
|
|
199
|
+
pkg_root = Path(__file__).parent.parent.parent.parent # src/../.. = repo root
|
|
200
|
+
config_dir = pkg_root / "config" / "agents"
|
|
201
|
+
|
|
202
|
+
self.config_dir = config_dir
|
|
203
|
+
|
|
204
|
+
def load_all(self) -> Dict[str, AgentDomainConfig]:
|
|
205
|
+
"""Load all agent configs from config_dir.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Dict mapping domain name to validated AgentDomainConfig.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
AgentConfigError: If any config file is invalid.
|
|
212
|
+
"""
|
|
213
|
+
if not self.config_dir.exists():
|
|
214
|
+
logger.warning("Agent config directory not found: %s", self.config_dir)
|
|
215
|
+
return {}
|
|
216
|
+
|
|
217
|
+
configs: Dict[str, AgentDomainConfig] = {}
|
|
218
|
+
|
|
219
|
+
for yaml_file in sorted(self.config_dir.glob("*.yaml")):
|
|
220
|
+
try:
|
|
221
|
+
config = self._load_file(yaml_file)
|
|
222
|
+
configs[config.name] = config
|
|
223
|
+
logger.debug("Loaded agent config: %s from %s", config.name, yaml_file.name)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
raise AgentConfigError(
|
|
226
|
+
f"Failed to load agent config from {yaml_file.name}: {e}"
|
|
227
|
+
) from e
|
|
228
|
+
|
|
229
|
+
logger.info("Loaded %d agent configs from %s", len(configs), self.config_dir)
|
|
230
|
+
return configs
|
|
231
|
+
|
|
232
|
+
def load_one(self, name: str) -> AgentDomainConfig:
|
|
233
|
+
"""Load a single agent config by domain name.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
name: Domain name (matches the 'name' field in YAML, not filename).
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
AgentConfigError: If the config is not found or invalid.
|
|
240
|
+
"""
|
|
241
|
+
yaml_file = self.config_dir / f"{name}.yaml"
|
|
242
|
+
if not yaml_file.exists():
|
|
243
|
+
raise AgentConfigError(f"Agent config not found: {yaml_file}")
|
|
244
|
+
return self._load_file(yaml_file)
|
|
245
|
+
|
|
246
|
+
def _load_file(self, path: Path) -> AgentDomainConfig:
|
|
247
|
+
"""Parse a single YAML file into AgentDomainConfig."""
|
|
248
|
+
with open(path) as f:
|
|
249
|
+
data = yaml.safe_load(f) or {}
|
|
250
|
+
try:
|
|
251
|
+
return AgentDomainConfig(**data)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
raise AgentConfigError(f"Invalid config in {path.name}: {e}") from e
|
|
254
|
+
|
|
255
|
+
def validate(
|
|
256
|
+
self,
|
|
257
|
+
config: AgentDomainConfig,
|
|
258
|
+
all_configs: Dict[str, AgentDomainConfig],
|
|
259
|
+
known_servers: Optional[List[str]] = None,
|
|
260
|
+
) -> List[str]:
|
|
261
|
+
"""Validate config against known servers and sibling configs.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
config: Config to validate.
|
|
265
|
+
all_configs: All other configs (for handoff target validation).
|
|
266
|
+
known_servers: List of known ResourceServer names from platform.
|
|
267
|
+
If None, server validation is skipped.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of validation error messages (empty if valid).
|
|
271
|
+
"""
|
|
272
|
+
errors: List[str] = []
|
|
273
|
+
|
|
274
|
+
# Validate resource_servers against known platform servers
|
|
275
|
+
if known_servers is not None:
|
|
276
|
+
for server in config.resource_servers:
|
|
277
|
+
if server not in known_servers:
|
|
278
|
+
errors.append(
|
|
279
|
+
f"resource_server '{server}' not found in platform. "
|
|
280
|
+
f"Available: {known_servers}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Validate handoff_targets reference real agent configs
|
|
284
|
+
for target in config.handoff_targets:
|
|
285
|
+
if target == config.name:
|
|
286
|
+
errors.append(f"handoff_target '{target}' cannot reference itself")
|
|
287
|
+
elif target not in all_configs:
|
|
288
|
+
errors.append(
|
|
289
|
+
f"handoff_target '{target}' not found in loaded agent configs. "
|
|
290
|
+
f"Available: {list(all_configs.keys())}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return errors
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AgentFactory — creates DomainAgent instances from AgentDomainConfig.
|
|
4
|
+
|
|
5
|
+
One factory per platform session. Receives the platform instance and a single
|
|
6
|
+
shared LLM (from llm_factory.resolve_llm). All domain agents share the same
|
|
7
|
+
LLM — model choice is a user-level concern, not per-agent.
|
|
8
|
+
|
|
9
|
+
Persistent agents share one AsyncSqliteSaver at ~/.apex/conversations.db,
|
|
10
|
+
with thread isolation via domain-prefixed thread IDs.
|
|
11
|
+
Stateless agents get no checkpointer — fresh context each session.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Dict, Optional
|
|
19
|
+
|
|
20
|
+
from langgraph.prebuilt import create_react_agent
|
|
21
|
+
|
|
22
|
+
from apex_core.agents.agent_config import AgentDomainConfig
|
|
23
|
+
from apex_core.agents.domain_agent import DomainAgent
|
|
24
|
+
from apex_core.agents.tool_wrapper import build_domain_tools
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from langchain_core.language_models import BaseChatModel
|
|
28
|
+
from apex_core.core.platform import APEXPlatform
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_GENERIC_SYSTEM_PROMPT = """You are an APEX Platform domain agent for the {domain} domain.
|
|
33
|
+
|
|
34
|
+
You help users manage {description}.
|
|
35
|
+
|
|
36
|
+
Available tools cover: {servers}.
|
|
37
|
+
|
|
38
|
+
Guidelines:
|
|
39
|
+
- Always confirm intent before executing destructive operations
|
|
40
|
+
- Default to dry-run (plan) mode unless user explicitly requests apply/execute
|
|
41
|
+
- Be concise and precise — infrastructure tasks need accuracy
|
|
42
|
+
- If a request is outside your domain, say so clearly
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AgentFactory:
|
|
47
|
+
"""Creates DomainAgent instances from YAML config definitions.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
platform: Initialized APEXPlatform with resource servers loaded.
|
|
51
|
+
llm: Shared LangChain chat model (from llm_factory.resolve_llm()).
|
|
52
|
+
checkpointer: Optional pre-initialized AsyncSqliteSaver.
|
|
53
|
+
If None, created lazily at ~/.apex/conversations.db
|
|
54
|
+
when the first persistent agent is requested.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
platform: "APEXPlatform",
|
|
60
|
+
llm: "BaseChatModel",
|
|
61
|
+
checkpointer=None,
|
|
62
|
+
):
|
|
63
|
+
self.platform = platform
|
|
64
|
+
self.llm = llm
|
|
65
|
+
self._checkpointer = checkpointer # shared, lazily initialized
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Public API
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
async def create_agent(self, config: AgentDomainConfig) -> DomainAgent:
|
|
72
|
+
"""Create a single domain agent from config.
|
|
73
|
+
|
|
74
|
+
Steps:
|
|
75
|
+
1. Build middleware-compliant tool wrappers for all resource servers
|
|
76
|
+
2. Load SKILL.md content (if configured)
|
|
77
|
+
3. Build system prompt
|
|
78
|
+
4. Create LangGraph ReAct agent with checkpointer
|
|
79
|
+
5. Return DomainAgent wrapper
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
AgentConfigError: If required resource servers are missing.
|
|
83
|
+
"""
|
|
84
|
+
# 1. Build tools — validates server availability
|
|
85
|
+
tools = build_domain_tools(config, self.platform)
|
|
86
|
+
if not tools:
|
|
87
|
+
logger.warning(
|
|
88
|
+
"Agent '%s' has no tools — resource servers %s may not be loaded",
|
|
89
|
+
config.name,
|
|
90
|
+
config.resource_servers,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 2. Load skill content
|
|
94
|
+
skill_content = self._load_skill_content(config.skill)
|
|
95
|
+
|
|
96
|
+
# 3. Build system prompt
|
|
97
|
+
system_prompt = self._build_system_prompt(config, skill_content)
|
|
98
|
+
|
|
99
|
+
# 4. Get checkpointer (shared for persistent, None for stateless)
|
|
100
|
+
checkpointer = await self._get_checkpointer(config)
|
|
101
|
+
|
|
102
|
+
# 5. Create LangGraph ReAct agent
|
|
103
|
+
graph = create_react_agent(
|
|
104
|
+
model=self.llm,
|
|
105
|
+
tools=tools,
|
|
106
|
+
state_modifier=system_prompt,
|
|
107
|
+
checkpointer=checkpointer,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
logger.info(
|
|
111
|
+
"Created domain agent '%s': %d tools, memory=%s, skill=%s",
|
|
112
|
+
config.name,
|
|
113
|
+
len(tools),
|
|
114
|
+
config.memory_mode,
|
|
115
|
+
config.skill or "none",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return DomainAgent(
|
|
119
|
+
config=config,
|
|
120
|
+
compiled_agent=graph,
|
|
121
|
+
tools=tools,
|
|
122
|
+
skill_prompt=skill_content or system_prompt,
|
|
123
|
+
active=True,
|
|
124
|
+
checkpointer=checkpointer,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def create_all(
|
|
128
|
+
self, configs: Dict[str, AgentDomainConfig]
|
|
129
|
+
) -> Dict[str, DomainAgent]:
|
|
130
|
+
"""Create all domain agents from configs.
|
|
131
|
+
|
|
132
|
+
Persistent agents are early-loaded at startup.
|
|
133
|
+
Stateless agents are created but marked inactive — they'll be
|
|
134
|
+
re-created per session when needed (lazy loading).
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dict mapping domain name → DomainAgent.
|
|
138
|
+
"""
|
|
139
|
+
agents: Dict[str, DomainAgent] = {}
|
|
140
|
+
|
|
141
|
+
for name, config in configs.items():
|
|
142
|
+
try:
|
|
143
|
+
agent = await self.create_agent(config)
|
|
144
|
+
agents[name] = agent
|
|
145
|
+
logger.info("Loaded agent: %s (%s)", name, config.memory_mode)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error("Failed to create agent '%s': %s", name, e)
|
|
148
|
+
# Continue loading other agents — partial startup is better than full failure
|
|
149
|
+
|
|
150
|
+
logger.info("AgentFactory: loaded %d/%d agents", len(agents), len(configs))
|
|
151
|
+
return agents
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
# Internal helpers
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def _build_system_prompt(
|
|
158
|
+
self, config: AgentDomainConfig, skill_content: Optional[str]
|
|
159
|
+
) -> str:
|
|
160
|
+
"""Build the system prompt for a domain agent.
|
|
161
|
+
|
|
162
|
+
Priority:
|
|
163
|
+
1. config.system_prompt (explicit YAML override)
|
|
164
|
+
2. SKILL.md content (if skill configured and file exists)
|
|
165
|
+
3. Generic domain prompt template
|
|
166
|
+
"""
|
|
167
|
+
if config.system_prompt:
|
|
168
|
+
return config.system_prompt
|
|
169
|
+
|
|
170
|
+
if skill_content:
|
|
171
|
+
return (
|
|
172
|
+
f"You are an APEX Platform domain agent for the {config.name} domain.\n\n"
|
|
173
|
+
f"Domain expertise (from SKILL.md):\n{skill_content}\n\n"
|
|
174
|
+
f"Available resource servers: {', '.join(config.resource_servers)}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return _GENERIC_SYSTEM_PROMPT.format(
|
|
178
|
+
domain=config.name,
|
|
179
|
+
description=config.description,
|
|
180
|
+
servers=", ".join(config.resource_servers),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _load_skill_content(self, skill_name: Optional[str]) -> Optional[str]:
|
|
184
|
+
"""Load SKILL.md content from ~/.apex/skills/{skill_name}/SKILL.md.
|
|
185
|
+
|
|
186
|
+
Returns None if skill is not configured or file is missing.
|
|
187
|
+
Logs a warning if configured but missing (user should install it).
|
|
188
|
+
"""
|
|
189
|
+
if not skill_name:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
skill_path = Path.home() / ".apex" / "skills" / skill_name / "SKILL.md"
|
|
193
|
+
if skill_path.exists():
|
|
194
|
+
try:
|
|
195
|
+
content = skill_path.read_text(encoding="utf-8")
|
|
196
|
+
logger.info("Loaded SKILL.md for '%s' from %s", skill_name, skill_path)
|
|
197
|
+
return content
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.warning("Failed to read SKILL.md at %s: %s", skill_path, e)
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
logger.warning(
|
|
203
|
+
"SKILL.md not found for skill '%s' at %s. "
|
|
204
|
+
"Install with: apex skills install %s",
|
|
205
|
+
skill_name,
|
|
206
|
+
skill_path,
|
|
207
|
+
skill_name,
|
|
208
|
+
)
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
async def _get_checkpointer(self, config: AgentDomainConfig):
|
|
212
|
+
"""Return the shared checkpointer for persistent agents, None for stateless.
|
|
213
|
+
|
|
214
|
+
All persistent agents share one AsyncSqliteSaver at ~/.apex/conversations.db.
|
|
215
|
+
Conversation isolation is via thread_id = "{domain}_{session_id}".
|
|
216
|
+
"""
|
|
217
|
+
if config.memory_mode != "persistent":
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
if self._checkpointer is None:
|
|
221
|
+
self._checkpointer = await self._init_checkpointer()
|
|
222
|
+
|
|
223
|
+
return self._checkpointer
|
|
224
|
+
|
|
225
|
+
async def _init_checkpointer(self):
|
|
226
|
+
"""Initialize shared AsyncSqliteSaver at ~/.apex/conversations.db."""
|
|
227
|
+
try:
|
|
228
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
229
|
+
|
|
230
|
+
db_path = Path.home() / ".apex" / "conversations.db"
|
|
231
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
|
|
233
|
+
checkpointer = AsyncSqliteSaver.from_conn_string(str(db_path))
|
|
234
|
+
logger.info("Initialized shared checkpointer at %s", db_path)
|
|
235
|
+
return checkpointer
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error("Failed to initialize checkpointer: %s", e)
|
|
239
|
+
logger.warning("Persistent agents will run without memory this session")
|
|
240
|
+
return None
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DomainAgent — wraps a compiled LangGraph ReAct agent for a single domain.
|
|
4
|
+
|
|
5
|
+
Each domain agent:
|
|
6
|
+
- Owns a scoped set of tools (only its resource servers' tools)
|
|
7
|
+
- Has persistent or stateless memory (via SqliteSaver checkpointer)
|
|
8
|
+
- Uses domain-prefixed thread IDs for conversation isolation:
|
|
9
|
+
thread_id = "{domain}_{session_id}"
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from langchain_core.messages import HumanMessage
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from langchain_core.tools import StructuredTool
|
|
22
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
23
|
+
|
|
24
|
+
from apex_core.agents.agent_config import AgentDomainConfig
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class DomainAgent:
|
|
31
|
+
"""Runtime wrapper for a compiled LangGraph ReAct agent serving one domain.
|
|
32
|
+
|
|
33
|
+
Created by AgentFactory. Not persisted — rebuilt from config at startup.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
config: "AgentDomainConfig"
|
|
37
|
+
compiled_agent: "CompiledStateGraph"
|
|
38
|
+
tools: List["StructuredTool"]
|
|
39
|
+
skill_prompt: str
|
|
40
|
+
active: bool = True
|
|
41
|
+
checkpointer: Optional[Any] = field(default=None, repr=False)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
return self.config.name
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def description(self) -> str:
|
|
49
|
+
return self.config.description
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def memory_mode(self) -> str:
|
|
53
|
+
return self.config.memory_mode
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_persistent(self) -> bool:
|
|
57
|
+
return self.config.memory_mode == "persistent"
|
|
58
|
+
|
|
59
|
+
def thread_id(self, session_id: str) -> str:
|
|
60
|
+
"""Construct domain-scoped thread ID for checkpoint isolation.
|
|
61
|
+
|
|
62
|
+
All conversations with this domain agent under the same session
|
|
63
|
+
share one checkpoint thread, keeping conversation context across turns.
|
|
64
|
+
"""
|
|
65
|
+
return f"{self.name}_{session_id}"
|
|
66
|
+
|
|
67
|
+
async def invoke(self, user_input: str, session_id: str) -> str:
|
|
68
|
+
"""Process a single user message and return the agent's response.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
user_input: Natural language query from the user.
|
|
72
|
+
session_id: Session identifier for multi-turn memory.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Agent response as a plain string.
|
|
76
|
+
"""
|
|
77
|
+
config = {"configurable": {"thread_id": self.thread_id(session_id)}}
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
result = await self.compiled_agent.ainvoke(
|
|
81
|
+
{"messages": [HumanMessage(content=user_input)]},
|
|
82
|
+
config=config,
|
|
83
|
+
)
|
|
84
|
+
# Extract last AI message content
|
|
85
|
+
messages = result.get("messages", [])
|
|
86
|
+
if messages:
|
|
87
|
+
last = messages[-1]
|
|
88
|
+
content = last.content if hasattr(last, "content") else str(last)
|
|
89
|
+
return content if isinstance(content, str) else str(content)
|
|
90
|
+
return "No response from agent."
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error("DomainAgent '%s' invocation failed: %s", self.name, e)
|
|
94
|
+
return f"Error: {e}"
|
|
95
|
+
|
|
96
|
+
def get_info(self) -> Dict[str, Any]:
|
|
97
|
+
"""Return agent metadata for display / info commands."""
|
|
98
|
+
return {
|
|
99
|
+
"name": self.name,
|
|
100
|
+
"description": self.description,
|
|
101
|
+
"memory_mode": self.memory_mode,
|
|
102
|
+
"resource_servers": self.config.resource_servers,
|
|
103
|
+
"tool_count": len(self.tools),
|
|
104
|
+
"tool_names": [t.name for t in self.tools],
|
|
105
|
+
"routing_keywords": self.config.routing_keywords,
|
|
106
|
+
"handoff_targets": self.config.handoff_targets,
|
|
107
|
+
"skill": self.config.skill,
|
|
108
|
+
"active": self.active,
|
|
109
|
+
}
|