Loading src/agentic/Config.py +29 −0 Original line number Diff line number Diff line Loading @@ -38,6 +38,13 @@ DEFAULT_OLLAMA_PROXY_LISTEN_PORT = "11434" DEFAULT_OLLAMA_PROXY_TARGET_PORT = "11434" DEFAULT_TFS_CONTEXT_UUID = "admin" DEFAULT_TFS_TOPOLOGY_UUID = "admin" DUMMY_DETERMINISTIC_MODELS = { "ci", "deterministic", "dummy", "dummy-deterministic", "none", } ENVVAR_ADK_A2A_PUBLIC_URL = "ADK_A2A_PUBLIC_URL" ENVVAR_ADK_A2A_TIMEOUT_SECONDS = "ADK_A2A_TIMEOUT_SECONDS" Loading Loading @@ -101,6 +108,28 @@ def get_agentic_model() -> str: return get_setting(ENVVAR_ADK_MODEL, default=DEFAULT_AGENTIC_MODEL) def get_openai_api_key() -> str: return get_setting(ENVVAR_OPENAI_API_KEY, default="").strip() def is_agentic_dummy_deterministic_mode() -> bool: return get_agentic_model().strip().lower() in DUMMY_DETERMINISTIC_MODELS def validate_agentic_llm_configuration() -> None: model_name = get_agentic_model().strip() if not model_name: raise RuntimeError("ADK_MODEL must be configured") if is_agentic_dummy_deterministic_mode(): return if model_name.startswith("openai/") and not get_openai_api_key(): raise RuntimeError( "OPENAI_API_KEY must be configured for OpenAI-backed " "Agentic models. Set ADK_MODEL=dummy-deterministic only " "for CI tests that must not contact an LLM provider." ) def get_agentic_graph() -> str: return get_setting(ENVVAR_ADK_AGENT_GRAPH, default=DEFAULT_AGENTIC_GRAPH) Loading src/agentic/README.md +197 −10 Original line number Diff line number Diff line # TFS Agentic Component The `agentic` component exposes a per-domain MD-NAF Agentic Module as a TeraFlowSDN service. It runs the Google ADK based runtime, accesses the local TeraFlowSDN controller through the `mcp_server` component, and exposes HTTP and A2A endpoints for operator requests and peer-domain coordination. The `agentic` component exposes a TeraFlowSDN Agentic Module as a controller service. One instance is normally co-located with one local TFS controller and is responsible for that controller domain. It uses MCP to access the local controller and standardized A2A to coordinate with peer Agentic instances when a request spans multiple controller domains. The runtime combines: - LLM-based intent interpretation and answer composition. - Deterministic workflows for controller-facing operations. - MCP tools for local TFS inventory, service, connection, and optical resource operations. - A2A peer actions for cross-domain inventory, spectrum negotiation, provisioning, teardown, and rollback. The controller remains the source of truth. Agentic does not invent topology, endpoint, spectrum, service, or connection state. ## Deployment Flavor Deploy Agentic after the core controller, Optical Controller, NBI, and MCP Server components are available: ```bash cd ~/tfs-ctrl TFS_COMPONENTS="context device pathcomp opticalcontroller service nbi webui mcp_server agentic" \ CRDB_DROP_DATABASE_IF_EXISTS=YES \ ./deploy/all.sh ``` The deployment manifest creates: - `agentic-config`: non-secret runtime configuration. - `agentic-secrets`: LLM and MCP secret material. - `agentic-data`: persistent volume claim for SQLite session state. - `agenticservice`: HTTP service on port `8800`. - `tfs-ingress-agentic`: ingress path `/agentic`. Do not commit real LLM API keys, MCP tokens, SSH keys, or controller credentials. Patch secrets at deployment time: ```bash kubectl create secret generic agentic-secrets \ --namespace tfs \ --from-literal=OPENAI_API_KEY="$OPENAI_API_KEY" \ --from-literal=TFS_MCP_AUTH_TOKEN="${TFS_MCP_AUTH_TOKEN:-}" \ --dry-run=client -o yaml | kubectl apply -f - kubectl rollout restart deployment/agenticservice -n tfs kubectl rollout status deployment/agenticservice -n tfs ``` ## Runtime Configuration Loading @@ -27,6 +74,46 @@ Main settings: - `OPENAI_API_KEY`: LLM provider secret, supplied through Kubernetes Secret. - `TFS_MCP_AUTH_TOKEN`: optional MCP authentication token, supplied through Kubernetes Secret. - `OLLAMA_API_BASE`: optional Ollama endpoint when using an Ollama LiteLLM model name. - `ADK_A2A_PUBLIC_URL`: public URL advertised in the A2A Agent Card. - `ADK_AGENT_RUN_TIMEOUT_SECONDS`: request timeout for LLM-backed execution. - `ADK_AGENT_STARTUP_WARMUP_ENABLED`: enables safe non-mutating warm-up prompts to absorb cold-start latency. - `ADK_AGENT_STARTUP_WARMUP_PROMPTS`: `||`-separated warm-up prompts. ## LLM Configuration And Fail-Fast Checks Agentic validates the LLM configuration during FastAPI startup. If the model is OpenAI-backed, for example `openai/gpt-4.1-mini`, `OPENAI_API_KEY` must be present and non-empty. Missing credentials cause the pod to fail startup instead of running and failing later on the first user request. For CI or offline tests that must not contact any LLM provider, set: ```yaml ADK_MODEL: "dummy-deterministic" ``` This mode bypasses LLM warm-up and exposes a narrow deterministic path for basic device-inventory prompts. It is intended for component and deployment tests only, not for research experiments or production-like validation. Valid examples: ```yaml ADK_MODEL: "openai/gpt-4.1-mini" OPENAI_API_KEY: "<provided through Kubernetes Secret>" ``` ```yaml ADK_MODEL: "ollama_chat/qwen2.5:7b-instruct" OLLAMA_API_BASE: "http://<ollama-host>:11434" ``` ```yaml ADK_MODEL: "dummy-deterministic" ``` ## Source Layout Loading @@ -41,12 +128,101 @@ The component source is split into: ## Exposed Endpoints - `GET /health`: component health and configured peer summary. - `POST /agent/run`: operator-facing natural-language request endpoint. - `GET /.well-known/agent-card.json`: A2A Agent Card. - `POST /a2a`: A2A JSON-RPC endpoint for peer MD-NAF modules. - Inventory, spectrum, and optical service workflow helper endpoints are exposed for diagnostics and integration testing. `GET /health` returns component health, local domain ID, peer summary, session database health, and startup warm-up results. `POST /agent/run` receives operator-facing natural-language requests: ```bash curl -X POST http://<controller-host>/agentic/agent/run \ -H 'Content-Type: application/json' \ -d '{ "prompt": "list existing devices", "user_id": "demo", "session_id": "demo-1" }' ``` `GET /.well-known/agent-card.json` exposes the A2A Agent Card. `POST /a2a` exposes the standardized A2A JSON-RPC endpoint used by peer Agentic instances. Diagnostic endpoints are also available for direct workflow tests: - `GET /inventory/devices` - `GET /domains` - `POST /inventory/domain` - `POST /spectrum/candidates` - `POST /spectrum/reserve` - `POST /spectrum/update` - `POST /services/optical` - `POST /services/delete` - `POST /services/delete-by-request` - `POST /workflows/cross-domain-optical` - `POST /workflows/cross-domain-optical/delete` Prefer `/agent/run` for operator-facing validation. Use direct diagnostic endpoints only for integration tests and failure isolation. ## Smoke Tests After deployment, check health: ```bash curl http://<controller-host>/agentic/health ``` Load an emulated scenario into the local controller: ```bash PYENV_VERSION=tfs ./src/tests/tools/load_scenario/run.sh \ src/tests/tools/load_scenario/example_descriptors.json ``` Verify MCP-backed Agentic inventory through the LLM path: ```bash curl -X POST http://<controller-host>/agentic/agent/run \ -H 'Content-Type: application/json' \ -d '{ "prompt": "list existing devices", "user_id": "smoke", "session_id": "smoke-1" }' ``` With the example descriptor, the expected answer reports seven local devices: `R1`, `R2`, `R3`, `R4`, `R5`, `R6`, and `R7`. For optical service tuning tests, use the OFC24 optical descriptor: ```bash PYENV_VERSION=tfs ./src/tests/tools/load_scenario/run.sh \ src/tests/ofc24/descriptors/topology.json ``` The companion payloads under `src/tests/ofc24/descriptors/` are useful as controller-facing references: - `service-unidir.json`: unidirectional optical connectivity service. - `service-bidir.json`: bidirectional optical connectivity service example. After loading the optical topology, request an optical connectivity service with explicit sizing: ```bash curl -X POST http://<controller-host>/agentic/agent/run \ -H 'Content-Type: application/json' \ -d '{ "prompt": "create a 50 GHz optical service from DC1-TP1 to DC2-TP1", "user_id": "smoke", "session_id": "smoke-2" }' ``` A successful mutating workflow must report the created TFS service UUIDs and verify that every segment reaches `SERVICESTATUS_ACTIVE`. ## Tests Loading @@ -54,3 +230,14 @@ Unit tests validate configuration parsing, peer parsing, and deterministic spectrum sizing. Integration tests use an in-process mocked MCP session, mocked MCP workflow replies, and a mocked ADK runner so CI can validate the Agentic component without a live controller or LLM API key. Useful local checks: ```bash PYTHONPATH=src python -m compileall -q src/agentic PYTHONPATH=src python -m pytest -q src/agentic/tests/test_*.py ``` The CI-safe no-LLM configuration path is validated through `ADK_MODEL=dummy-deterministic`. Normal deployment with OpenAI-backed models must provide `OPENAI_API_KEY`; otherwise startup fails intentionally. src/agentic/service/a2a_transport.py +21 −21 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Standards-based A2A transport for MD-NAF ADK domain workflows.""" """Standards-based A2A transport for TFS Agentic domain workflows.""" from __future__ import annotations Loading Loading @@ -84,13 +84,13 @@ def _domain_api_base_url() -> str: def build_agent_card() -> AgentCard: """Build the standards-compliant A2A Agent Card for one MD-NAF domain.""" """Build the standards-compliant A2A Agent Card for one domain.""" base_url = _domain_api_base_url() return AgentCard( name=f"md-naf-domain-{DOMAIN_ID.lower()}", name=f"tfs-agentic-domain-{DOMAIN_ID.lower()}", description=( "MD-NAF domain agent exposing TFS inventory, flex-grid spectrum " "TFS Agentic domain agent exposing TFS inventory, flex-grid " "negotiation, optical service provisioning, and service teardown " "through the A2A protocol." ), Loading @@ -106,46 +106,46 @@ def build_agent_card() -> AgentCard: defaultOutputModes=["application/json"], skills=[ AgentSkill( id="mdnaf.inventory", id="tfs.inventory", name="Remote domain inventory interrogation", description=( "List devices, endpoints, links, optical links, services, " "and connections in this domain." ), tags=["md-naf", "inventory", "tfs", "mcp"], tags=["tfs", "agentic", "inventory", "mcp"], inputModes=["application/json"], outputModes=["application/json"], ), AgentSkill( id="mdnaf.spectrum", id="tfs.spectrum", name="Flex-grid spectrum negotiation", description=( "Compute candidates, reserve spectrum, and update " "spectrum reservation state." ), tags=["md-naf", "optical", "spectrum", "flex-grid"], tags=["tfs", "agentic", "optical", "spectrum", "flex-grid"], inputModes=["application/json"], outputModes=["application/json"], ), AgentSkill( id="mdnaf.optical-service", id="tfs.optical-service", name="Optical connectivity service lifecycle", description=( "Create and delete TFS optical connectivity service " "segments in this domain." ), tags=["md-naf", "optical", "service", "tfs"], tags=["tfs", "agentic", "optical", "service"], inputModes=["application/json"], outputModes=["application/json"], ), AgentSkill( id="mdnaf.cross-domain-workflow", id="tfs.cross-domain-workflow", name="Cross-domain optical workflow delegation", description=( "Execute or continue a cross-domain optical service " "workflow through this domain." ), tags=["md-naf", "a2a", "cross-domain", "workflow"], tags=["tfs", "agentic", "a2a", "cross-domain", "workflow"], inputModes=["application/json"], outputModes=["application/json"], ), Loading @@ -158,7 +158,7 @@ def install_a2a_routes(app: Any) -> None: """Mount A2A SDK routes onto an existing FastAPI/Starlette app.""" request_handler = DefaultRequestHandler( agent_executor=MdNafDomainA2AExecutor(), agent_executor=TfsAgenticDomainA2AExecutor(), task_store=InMemoryTaskStore(), ) a2a_app = A2AStarletteApplication( Loading Loading @@ -338,7 +338,7 @@ async def call_a2a_action( data={ "action": action, "payload": payload or {}, "protocol": "md-naf-a2a-v1", "protocol": "tfs-agentic-a2a-v1", "requesting_domain": DOMAIN_ID, } ) Loading Loading @@ -427,8 +427,8 @@ def _extract_message_data(message: Message) -> Dict[str, Any] | None: return None class MdNafDomainA2AExecutor(AgentExecutor): """A2A SDK executor exposing deterministic MD-NAF domain skills.""" class TfsAgenticDomainA2AExecutor(AgentExecutor): """A2A SDK executor exposing deterministic TFS Agentic domain skills.""" async def execute( self, Loading @@ -448,7 +448,7 @@ class MdNafDomainA2AExecutor(AgentExecutor): if isinstance(result, dict) else True ), "protocol": "md-naf-a2a-v1", "protocol": "tfs-agentic-a2a-v1", "domain_id": DOMAIN_ID, "domain_name": DOMAIN_NAME, "action": action, Loading @@ -462,7 +462,7 @@ class MdNafDomainA2AExecutor(AgentExecutor): ) response = { "ok": False, "protocol": "md-naf-a2a-v1", "protocol": "tfs-agentic-a2a-v1", "domain_id": DOMAIN_ID, "domain_name": DOMAIN_NAME, "action": action, Loading @@ -480,7 +480,7 @@ class MdNafDomainA2AExecutor(AgentExecutor): _response_message( { "ok": False, "protocol": "md-naf-a2a-v1", "protocol": "tfs-agentic-a2a-v1", "domain_id": DOMAIN_ID, "error": "cancel_not_supported", }, Loading Loading @@ -508,7 +508,7 @@ async def dispatch_a2a_action( delete_local_services_for_request, delete_service, list_domain_inventory, list_mdnaf_domains, list_controller_domains, locate_device, release_tfs_spectrum_reservation, ) Loading Loading @@ -564,7 +564,7 @@ async def dispatch_a2a_action( bool(payload.get("include_optical_links", True)), ) if action == "domains.list": return await list_mdnaf_domains( return await list_controller_domains( include_devices=bool(payload.get("include_devices", True)), user_id=str(payload.get("user_id", "a2a")), session_id=str(payload.get("session_id", "a2a")), Loading src/agentic/service/agent.py +1 −1 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ADK entrypoint for the isolated MD-NAF runtime.""" """ADK entrypoint for the TFS Agentic runtime.""" from agentic.service.settings import AGENT_GRAPH Loading src/agentic/service/agents/granular_root.py +3 −3 Original line number Diff line number Diff line Loading @@ -25,7 +25,7 @@ from agentic.service.llm import build_litellm GRANULAR_ROOT_ROUTER_INSTRUCTION = """ You are the first MD-NAF request router. You are the first TFS Agentic request router. Choose only between: - retrieval_router_agent for read-only retrieval, listing, inspection, Loading @@ -43,8 +43,8 @@ LiteLlm.model_rebuild(force=True) root_agent = Agent( model=build_litellm(), name="md_naf_granular_root_router", description="Minimal MD-NAF root router for granular specialist agents.", name="tfs_agentic_granular_root_router", description="Minimal TFS Agentic root router for specialist agents.", instruction=GRANULAR_ROOT_ROUTER_INSTRUCTION, sub_agents=[retrieval_router_agent, mutation_router_agent], ) Loading
src/agentic/Config.py +29 −0 Original line number Diff line number Diff line Loading @@ -38,6 +38,13 @@ DEFAULT_OLLAMA_PROXY_LISTEN_PORT = "11434" DEFAULT_OLLAMA_PROXY_TARGET_PORT = "11434" DEFAULT_TFS_CONTEXT_UUID = "admin" DEFAULT_TFS_TOPOLOGY_UUID = "admin" DUMMY_DETERMINISTIC_MODELS = { "ci", "deterministic", "dummy", "dummy-deterministic", "none", } ENVVAR_ADK_A2A_PUBLIC_URL = "ADK_A2A_PUBLIC_URL" ENVVAR_ADK_A2A_TIMEOUT_SECONDS = "ADK_A2A_TIMEOUT_SECONDS" Loading Loading @@ -101,6 +108,28 @@ def get_agentic_model() -> str: return get_setting(ENVVAR_ADK_MODEL, default=DEFAULT_AGENTIC_MODEL) def get_openai_api_key() -> str: return get_setting(ENVVAR_OPENAI_API_KEY, default="").strip() def is_agentic_dummy_deterministic_mode() -> bool: return get_agentic_model().strip().lower() in DUMMY_DETERMINISTIC_MODELS def validate_agentic_llm_configuration() -> None: model_name = get_agentic_model().strip() if not model_name: raise RuntimeError("ADK_MODEL must be configured") if is_agentic_dummy_deterministic_mode(): return if model_name.startswith("openai/") and not get_openai_api_key(): raise RuntimeError( "OPENAI_API_KEY must be configured for OpenAI-backed " "Agentic models. Set ADK_MODEL=dummy-deterministic only " "for CI tests that must not contact an LLM provider." ) def get_agentic_graph() -> str: return get_setting(ENVVAR_ADK_AGENT_GRAPH, default=DEFAULT_AGENTIC_GRAPH) Loading
src/agentic/README.md +197 −10 Original line number Diff line number Diff line # TFS Agentic Component The `agentic` component exposes a per-domain MD-NAF Agentic Module as a TeraFlowSDN service. It runs the Google ADK based runtime, accesses the local TeraFlowSDN controller through the `mcp_server` component, and exposes HTTP and A2A endpoints for operator requests and peer-domain coordination. The `agentic` component exposes a TeraFlowSDN Agentic Module as a controller service. One instance is normally co-located with one local TFS controller and is responsible for that controller domain. It uses MCP to access the local controller and standardized A2A to coordinate with peer Agentic instances when a request spans multiple controller domains. The runtime combines: - LLM-based intent interpretation and answer composition. - Deterministic workflows for controller-facing operations. - MCP tools for local TFS inventory, service, connection, and optical resource operations. - A2A peer actions for cross-domain inventory, spectrum negotiation, provisioning, teardown, and rollback. The controller remains the source of truth. Agentic does not invent topology, endpoint, spectrum, service, or connection state. ## Deployment Flavor Deploy Agentic after the core controller, Optical Controller, NBI, and MCP Server components are available: ```bash cd ~/tfs-ctrl TFS_COMPONENTS="context device pathcomp opticalcontroller service nbi webui mcp_server agentic" \ CRDB_DROP_DATABASE_IF_EXISTS=YES \ ./deploy/all.sh ``` The deployment manifest creates: - `agentic-config`: non-secret runtime configuration. - `agentic-secrets`: LLM and MCP secret material. - `agentic-data`: persistent volume claim for SQLite session state. - `agenticservice`: HTTP service on port `8800`. - `tfs-ingress-agentic`: ingress path `/agentic`. Do not commit real LLM API keys, MCP tokens, SSH keys, or controller credentials. Patch secrets at deployment time: ```bash kubectl create secret generic agentic-secrets \ --namespace tfs \ --from-literal=OPENAI_API_KEY="$OPENAI_API_KEY" \ --from-literal=TFS_MCP_AUTH_TOKEN="${TFS_MCP_AUTH_TOKEN:-}" \ --dry-run=client -o yaml | kubectl apply -f - kubectl rollout restart deployment/agenticservice -n tfs kubectl rollout status deployment/agenticservice -n tfs ``` ## Runtime Configuration Loading @@ -27,6 +74,46 @@ Main settings: - `OPENAI_API_KEY`: LLM provider secret, supplied through Kubernetes Secret. - `TFS_MCP_AUTH_TOKEN`: optional MCP authentication token, supplied through Kubernetes Secret. - `OLLAMA_API_BASE`: optional Ollama endpoint when using an Ollama LiteLLM model name. - `ADK_A2A_PUBLIC_URL`: public URL advertised in the A2A Agent Card. - `ADK_AGENT_RUN_TIMEOUT_SECONDS`: request timeout for LLM-backed execution. - `ADK_AGENT_STARTUP_WARMUP_ENABLED`: enables safe non-mutating warm-up prompts to absorb cold-start latency. - `ADK_AGENT_STARTUP_WARMUP_PROMPTS`: `||`-separated warm-up prompts. ## LLM Configuration And Fail-Fast Checks Agentic validates the LLM configuration during FastAPI startup. If the model is OpenAI-backed, for example `openai/gpt-4.1-mini`, `OPENAI_API_KEY` must be present and non-empty. Missing credentials cause the pod to fail startup instead of running and failing later on the first user request. For CI or offline tests that must not contact any LLM provider, set: ```yaml ADK_MODEL: "dummy-deterministic" ``` This mode bypasses LLM warm-up and exposes a narrow deterministic path for basic device-inventory prompts. It is intended for component and deployment tests only, not for research experiments or production-like validation. Valid examples: ```yaml ADK_MODEL: "openai/gpt-4.1-mini" OPENAI_API_KEY: "<provided through Kubernetes Secret>" ``` ```yaml ADK_MODEL: "ollama_chat/qwen2.5:7b-instruct" OLLAMA_API_BASE: "http://<ollama-host>:11434" ``` ```yaml ADK_MODEL: "dummy-deterministic" ``` ## Source Layout Loading @@ -41,12 +128,101 @@ The component source is split into: ## Exposed Endpoints - `GET /health`: component health and configured peer summary. - `POST /agent/run`: operator-facing natural-language request endpoint. - `GET /.well-known/agent-card.json`: A2A Agent Card. - `POST /a2a`: A2A JSON-RPC endpoint for peer MD-NAF modules. - Inventory, spectrum, and optical service workflow helper endpoints are exposed for diagnostics and integration testing. `GET /health` returns component health, local domain ID, peer summary, session database health, and startup warm-up results. `POST /agent/run` receives operator-facing natural-language requests: ```bash curl -X POST http://<controller-host>/agentic/agent/run \ -H 'Content-Type: application/json' \ -d '{ "prompt": "list existing devices", "user_id": "demo", "session_id": "demo-1" }' ``` `GET /.well-known/agent-card.json` exposes the A2A Agent Card. `POST /a2a` exposes the standardized A2A JSON-RPC endpoint used by peer Agentic instances. Diagnostic endpoints are also available for direct workflow tests: - `GET /inventory/devices` - `GET /domains` - `POST /inventory/domain` - `POST /spectrum/candidates` - `POST /spectrum/reserve` - `POST /spectrum/update` - `POST /services/optical` - `POST /services/delete` - `POST /services/delete-by-request` - `POST /workflows/cross-domain-optical` - `POST /workflows/cross-domain-optical/delete` Prefer `/agent/run` for operator-facing validation. Use direct diagnostic endpoints only for integration tests and failure isolation. ## Smoke Tests After deployment, check health: ```bash curl http://<controller-host>/agentic/health ``` Load an emulated scenario into the local controller: ```bash PYENV_VERSION=tfs ./src/tests/tools/load_scenario/run.sh \ src/tests/tools/load_scenario/example_descriptors.json ``` Verify MCP-backed Agentic inventory through the LLM path: ```bash curl -X POST http://<controller-host>/agentic/agent/run \ -H 'Content-Type: application/json' \ -d '{ "prompt": "list existing devices", "user_id": "smoke", "session_id": "smoke-1" }' ``` With the example descriptor, the expected answer reports seven local devices: `R1`, `R2`, `R3`, `R4`, `R5`, `R6`, and `R7`. For optical service tuning tests, use the OFC24 optical descriptor: ```bash PYENV_VERSION=tfs ./src/tests/tools/load_scenario/run.sh \ src/tests/ofc24/descriptors/topology.json ``` The companion payloads under `src/tests/ofc24/descriptors/` are useful as controller-facing references: - `service-unidir.json`: unidirectional optical connectivity service. - `service-bidir.json`: bidirectional optical connectivity service example. After loading the optical topology, request an optical connectivity service with explicit sizing: ```bash curl -X POST http://<controller-host>/agentic/agent/run \ -H 'Content-Type: application/json' \ -d '{ "prompt": "create a 50 GHz optical service from DC1-TP1 to DC2-TP1", "user_id": "smoke", "session_id": "smoke-2" }' ``` A successful mutating workflow must report the created TFS service UUIDs and verify that every segment reaches `SERVICESTATUS_ACTIVE`. ## Tests Loading @@ -54,3 +230,14 @@ Unit tests validate configuration parsing, peer parsing, and deterministic spectrum sizing. Integration tests use an in-process mocked MCP session, mocked MCP workflow replies, and a mocked ADK runner so CI can validate the Agentic component without a live controller or LLM API key. Useful local checks: ```bash PYTHONPATH=src python -m compileall -q src/agentic PYTHONPATH=src python -m pytest -q src/agentic/tests/test_*.py ``` The CI-safe no-LLM configuration path is validated through `ADK_MODEL=dummy-deterministic`. Normal deployment with OpenAI-backed models must provide `OPENAI_API_KEY`; otherwise startup fails intentionally.
src/agentic/service/a2a_transport.py +21 −21 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Standards-based A2A transport for MD-NAF ADK domain workflows.""" """Standards-based A2A transport for TFS Agentic domain workflows.""" from __future__ import annotations Loading Loading @@ -84,13 +84,13 @@ def _domain_api_base_url() -> str: def build_agent_card() -> AgentCard: """Build the standards-compliant A2A Agent Card for one MD-NAF domain.""" """Build the standards-compliant A2A Agent Card for one domain.""" base_url = _domain_api_base_url() return AgentCard( name=f"md-naf-domain-{DOMAIN_ID.lower()}", name=f"tfs-agentic-domain-{DOMAIN_ID.lower()}", description=( "MD-NAF domain agent exposing TFS inventory, flex-grid spectrum " "TFS Agentic domain agent exposing TFS inventory, flex-grid " "negotiation, optical service provisioning, and service teardown " "through the A2A protocol." ), Loading @@ -106,46 +106,46 @@ def build_agent_card() -> AgentCard: defaultOutputModes=["application/json"], skills=[ AgentSkill( id="mdnaf.inventory", id="tfs.inventory", name="Remote domain inventory interrogation", description=( "List devices, endpoints, links, optical links, services, " "and connections in this domain." ), tags=["md-naf", "inventory", "tfs", "mcp"], tags=["tfs", "agentic", "inventory", "mcp"], inputModes=["application/json"], outputModes=["application/json"], ), AgentSkill( id="mdnaf.spectrum", id="tfs.spectrum", name="Flex-grid spectrum negotiation", description=( "Compute candidates, reserve spectrum, and update " "spectrum reservation state." ), tags=["md-naf", "optical", "spectrum", "flex-grid"], tags=["tfs", "agentic", "optical", "spectrum", "flex-grid"], inputModes=["application/json"], outputModes=["application/json"], ), AgentSkill( id="mdnaf.optical-service", id="tfs.optical-service", name="Optical connectivity service lifecycle", description=( "Create and delete TFS optical connectivity service " "segments in this domain." ), tags=["md-naf", "optical", "service", "tfs"], tags=["tfs", "agentic", "optical", "service"], inputModes=["application/json"], outputModes=["application/json"], ), AgentSkill( id="mdnaf.cross-domain-workflow", id="tfs.cross-domain-workflow", name="Cross-domain optical workflow delegation", description=( "Execute or continue a cross-domain optical service " "workflow through this domain." ), tags=["md-naf", "a2a", "cross-domain", "workflow"], tags=["tfs", "agentic", "a2a", "cross-domain", "workflow"], inputModes=["application/json"], outputModes=["application/json"], ), Loading @@ -158,7 +158,7 @@ def install_a2a_routes(app: Any) -> None: """Mount A2A SDK routes onto an existing FastAPI/Starlette app.""" request_handler = DefaultRequestHandler( agent_executor=MdNafDomainA2AExecutor(), agent_executor=TfsAgenticDomainA2AExecutor(), task_store=InMemoryTaskStore(), ) a2a_app = A2AStarletteApplication( Loading Loading @@ -338,7 +338,7 @@ async def call_a2a_action( data={ "action": action, "payload": payload or {}, "protocol": "md-naf-a2a-v1", "protocol": "tfs-agentic-a2a-v1", "requesting_domain": DOMAIN_ID, } ) Loading Loading @@ -427,8 +427,8 @@ def _extract_message_data(message: Message) -> Dict[str, Any] | None: return None class MdNafDomainA2AExecutor(AgentExecutor): """A2A SDK executor exposing deterministic MD-NAF domain skills.""" class TfsAgenticDomainA2AExecutor(AgentExecutor): """A2A SDK executor exposing deterministic TFS Agentic domain skills.""" async def execute( self, Loading @@ -448,7 +448,7 @@ class MdNafDomainA2AExecutor(AgentExecutor): if isinstance(result, dict) else True ), "protocol": "md-naf-a2a-v1", "protocol": "tfs-agentic-a2a-v1", "domain_id": DOMAIN_ID, "domain_name": DOMAIN_NAME, "action": action, Loading @@ -462,7 +462,7 @@ class MdNafDomainA2AExecutor(AgentExecutor): ) response = { "ok": False, "protocol": "md-naf-a2a-v1", "protocol": "tfs-agentic-a2a-v1", "domain_id": DOMAIN_ID, "domain_name": DOMAIN_NAME, "action": action, Loading @@ -480,7 +480,7 @@ class MdNafDomainA2AExecutor(AgentExecutor): _response_message( { "ok": False, "protocol": "md-naf-a2a-v1", "protocol": "tfs-agentic-a2a-v1", "domain_id": DOMAIN_ID, "error": "cancel_not_supported", }, Loading Loading @@ -508,7 +508,7 @@ async def dispatch_a2a_action( delete_local_services_for_request, delete_service, list_domain_inventory, list_mdnaf_domains, list_controller_domains, locate_device, release_tfs_spectrum_reservation, ) Loading Loading @@ -564,7 +564,7 @@ async def dispatch_a2a_action( bool(payload.get("include_optical_links", True)), ) if action == "domains.list": return await list_mdnaf_domains( return await list_controller_domains( include_devices=bool(payload.get("include_devices", True)), user_id=str(payload.get("user_id", "a2a")), session_id=str(payload.get("session_id", "a2a")), Loading
src/agentic/service/agent.py +1 −1 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ADK entrypoint for the isolated MD-NAF runtime.""" """ADK entrypoint for the TFS Agentic runtime.""" from agentic.service.settings import AGENT_GRAPH Loading
src/agentic/service/agents/granular_root.py +3 −3 Original line number Diff line number Diff line Loading @@ -25,7 +25,7 @@ from agentic.service.llm import build_litellm GRANULAR_ROOT_ROUTER_INSTRUCTION = """ You are the first MD-NAF request router. You are the first TFS Agentic request router. Choose only between: - retrieval_router_agent for read-only retrieval, listing, inspection, Loading @@ -43,8 +43,8 @@ LiteLlm.model_rebuild(force=True) root_agent = Agent( model=build_litellm(), name="md_naf_granular_root_router", description="Minimal MD-NAF root router for granular specialist agents.", name="tfs_agentic_granular_root_router", description="Minimal TFS Agentic root router for specialist agents.", instruction=GRANULAR_ROOT_ROUTER_INSTRUCTION, sub_agents=[retrieval_router_agent, mutation_router_agent], )