Commit 90b13e7b authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Document agentic and MCP server components

parent 73045b69
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
@@ -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"
@@ -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)

+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

@@ -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

@@ -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

@@ -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.
+21 −21
Original line number Diff line number Diff line
@@ -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

@@ -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."
        ),
@@ -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"],
            ),
@@ -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(
@@ -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,
                    }
                )
@@ -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,
@@ -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,
@@ -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,
@@ -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",
                },
@@ -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,
    )
@@ -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")),
+1 −1
Original line number Diff line number Diff line
@@ -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

+3 −3
Original line number Diff line number Diff line
@@ -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,
@@ -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