Metadata-Version: 2.4
Name: ncp-sdk
Version: 1.1.2
Summary: SDK for building and deploying AI agents on the NCP platform
Author: Aviz Networks
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: click>=8.0.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: toml>=0.10.2
Requires-Dist: requests>=2.31.0
Requires-Dist: rich>=13.0.0
Requires-Dist: urllib3>=2.0.0
Requires-Dist: websockets>=12.0
Requires-Dist: prompt-toolkit>=3.0.0
Requires-Dist: pathspec>=0.11.0
Dynamic: requires-python

# NCP SDK User Guide

**Comprehensive Guide to Network Copilot SDK for AI Agent Development**

The NCP SDK enables developers to create sophisticated AI agents and deploy them on the NCP platform. This guide covers everything from basic setup to advanced features like the agent memory store, file access, multi-agent composition, and MCP integrations.

---

## Table of Contents

1. [Getting Started](#getting-started)
   - [Prerequisites](#prerequisites)
   - [Installation](#installation)
   - [Quick Verification](#quick-verification)

2. [Core Concepts](#core-concepts)
   - [Tools](#tools)
   - [Agents](#agents)
   - [Project Structure](#project-structure)

3. [Advanced Features](#advanced-features)
   - [MCP Integration](#mcp-integration)
   - [Data Connectors](#data-connectors)
   - [Agent Memory](#agent-memory)
   - [Files API](#files-api)
   - [Knowledge Base Tools](#knowledge-base-tools)
   - [Multi-Agent Composition](#multi-agent-composition)
   - [Calling the LLM from a Tool](#calling-the-llm-from-a-tool)
   - [Generic SSH/HTTP Connectors](#generic-sshhttp-connectors)
   - [Metrics API](#metrics-api)

4. [Dependency Management](#dependency-management)
   - [Python Dependencies](#python-dependencies)
   - [System Dependencies](#system-dependencies)

5. [SDK Workflow](#sdk-workflow)
   - [Project Initialization](#project-initialization)
   - [Development](#development)
   - [Validation](#validation)
   - [Packaging](#packaging)
   - [Deployment](#deployment)
   - [Testing Deployed Agents](#testing-deployed-agents)
   - [Discovering Connectors and Platform Agents](#discovering-connectors-and-platform-agents)
   - [Custom Agent Onboarding (Admin-only)](#custom-agent-onboarding-admin-only)

6. [Examples](#examples)

---

## Getting Started

### Prerequisites

#### Python Version

- **Python 3.8 or higher** is required
- Python 3.9+ recommended for better type support

#### Platform-Specific Setup

##### macOS

```bash
# Install Python via Homebrew (recommended)
brew install python@3.11

# Or use pyenv for version management
brew install pyenv
pyenv install 3.11.0
pyenv global 3.11.0
```

##### Linux (Ubuntu/Debian)

```bash
# Update package list
sudo apt update

# Install Python and pip
sudo apt install python3.11 python3.11-pip python3.11-venv

# Verify installation
python3.11 --version
```

##### Windows

1. Download Python from [python.org](https://python.org)
2. Run installer and **check "Add Python to PATH"**
3. Open Command Prompt or PowerShell to verify:

```cmd
python --version
pip --version
```

#### Virtual Environment

```bash
# Already included with Python 3.3+
python -m venv --help
```

### Installation

#### Step 1: Create Virtual Environment

**Using venv:**

```bash
# Create virtual environment
python -m venv .venv

# Activate virtual environment
# On macOS/Linux:
source .venv/bin/activate

# On Windows:
.venv\Scripts\activate

# Verify activation (should show .venv in prompt)
which python
```

#### Step 2: Install NCP SDK

```bash
# Install from PyPI
pip install ncp-sdk
```

#### Step 3: Verify Installation

```bash
# Check if NCP CLI is available
ncp --help

# Check Python import
python -c "from ncp import Agent, tool; print('NCP SDK installed successfully!')"
```

### Quick Verification

Create a simple test to ensure everything works:

```python
# test_ncp.py
from ncp import Agent, tool

@tool
def hello_world(name: str = "World") -> str:
    """Say hello to someone."""
    return f"Hello, {name}!"

# This should work without errors
agent = Agent(
    name="TestAgent",
    description="A simple test agent",
    instructions="You are a test agent. Be helpful.",
    tools=[hello_world]
)

print("✅ NCP SDK is working correctly!")
```

Run the test:

```bash
python test_ncp.py
```

---

## Core Concepts

### Tools

Tools are the building blocks that give your agents capabilities. They're Python functions decorated with `@tool` that agents can call to perform actions. The decorator turns the function into a `Tool` instance (`.name`, `.description`, `.get_schema()`) by building an OpenAI-style function-calling schema from the signature's type hints and the docstring — both are required.

#### Basic Tool Creation

```python
from ncp import tool

@tool
def ping_device(ip_address: str, timeout: int = 5) -> dict:
    """Ping a network device to check connectivity.

    Args:
        ip_address: Target IP address to ping
        timeout: Timeout in seconds (default: 5)

    Returns:
        Dictionary with ping results and connectivity status
    """
    import subprocess
    import time

    start_time = time.time()
    try:
        result = subprocess.run(['ping', '-c', '1', '-W', str(timeout), ip_address],
                              capture_output=True, text=True)
        response_time = time.time() - start_time

        return {
            "ip_address": ip_address,
            "reachable": result.returncode == 0,
            "response_time_ms": round(response_time * 1000, 2),
            "raw_output": result.stdout.strip() if result.returncode == 0 else result.stderr.strip()
        }
    except Exception as e:
        return {
            "ip_address": ip_address,
            "reachable": False,
            "error": str(e)
        }
```

#### Async Tools

For operations that might take time (API calls, file operations), both sync and async functions are supported:

```python
@tool
async def fetch_data(url: str) -> dict:
    """Fetch data from a URL (async tools supported)."""
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.json()
```

#### Error Handling in Tools

```python
from ncp import tool
import logging

@tool
def get_interface_status(device_ip: str, interface_name: str) -> dict:
    """Get network interface status with proper error handling.

    Args:
        device_ip: IP address of the network device
        interface_name: Name of the interface (e.g., "GigabitEthernet0/1")

    Returns:
        Interface status information

    Raises:
        ConnectionError: If device is unreachable
        ValueError: If interface doesn't exist
    """
    try:
        if not interface_name or "/" not in interface_name:
            raise ValueError(f"Invalid interface name: {interface_name}")

        logging.info(f"Checking interface {interface_name} on {device_ip}")
        status = {
            "device_ip": device_ip,
            "interface": interface_name,
            "admin_status": "up",
            "operational_status": "up",
        }
        return status

    except ValueError as e:
        logging.error(f"Interface validation error: {e}")
        raise ValueError(f"Interface check failed: {str(e)}")
    except Exception as e:
        logging.error(f"Unexpected error checking interface: {e}")
        raise ConnectionError(f"Failed to connect to device {device_ip}: {str(e)}")
```

#### Tool Documentation Best Practices

```python
@tool
def search_documents(
    query: str,
    max_results: int = 10,
    include_metadata: bool = True
) -> List[dict]:
    """Search through documents using semantic search.

    Args:
        query: Search query string. Use natural language or keywords.
        max_results: Maximum number of results to return (1-100).
        include_metadata: Whether to include document metadata in results.

    Returns:
        List of dictionaries, each containing:
        - content: Document content excerpt
        - title: Document title
        - relevance_score: Similarity score (0.0-1.0)
        - metadata: Document metadata (if include_metadata=True)

    Examples:
        >>> search_documents("Python programming", max_results=5)
        [{"content": "...", "title": "...", "relevance_score": 0.92}]
    """
    # Implementation here
    pass
```

### Agents

Agents are AI entities that use tools to accomplish tasks. They combine language models with your custom tools, data connectors, MCP servers, and memory/UI configuration.

#### Basic Agent Configuration

```python
from ncp import Agent

agent = Agent(
    name="NetworkMonitorBot",
    description="AI assistant for network monitoring and diagnostics",
    instructions="""
    You are a network monitoring specialist. Your goal is to:

    1. Monitor network device health and connectivity
    2. Diagnose network issues using available tools
    3. Provide clear reports on network status
    4. Alert on any critical network problems

    Always verify device connectivity before performing other operations.
    """,
    tools=[ping_device, get_interface_status]
)
```

#### Full `Agent` Reference

`Agent` is a dataclass (`ncp/agent.py`) with these fields:

| Field | Default | Purpose |
|---|---|---|
| `name` | required | Agent name (must be unique within project) |
| `description` | required | Brief description of agent capabilities |
| `instructions` | required | System instructions defining agent behavior |
| `tools` | `[]` | List of `Tool` instances (created with `@tool`) |
| `connectors` | `[]` | Data connector names, resolved by the platform (see [Data Connectors](#data-connectors)) |
| `mcp_servers` | `[]` | List of `MCPConfig` for external tool servers |
| `llm_config` | `None` | Optional `LLMConfig` (platform defaults if not specified) |
| `memory_config` | `None` | Optional `MemoryConfig` — conversation short-term memory strategy (default: STM enabled, last 30 messages) |
| `memory_store_enabled` | `False` | When `True`, the executor intercepts large tool results, stores them in Redis, and replaces the full payload with a lightweight reference (8-char ID + row count + schema + preview). Prevents context overflow from large MCP/tool responses |
| `memory_tools_enabled` | `False` | When `True`, the platform auto-injects built-in memory tools (`retrieve_memory`, `list_memory_entries`, `get_memory_details`, `delete_memory`) |
| `memory_context_enabled` | `False` | When `True`, a table of all stored memory entries is injected into the system prompt before each LLM call, so the LLM knows what datasets exist without an explicit tool call |
| `ui_components_enabled` | `False` | When `True`, the platform attaches its UI visualization server (interactive tables, charts, stat cards, dashboards, report previews) so the agent can render results as HTML |
| `ui_components` | `None` | Optional allowlist of UI tool names to expose when `ui_components_enabled=True`. `None` exposes all; set a list (e.g. `["show_data_table_from_memory"]`) to restrict |
| `max_iterations` | `None` | Optional cap on the tool-calling loop (one iteration = one LLM turn that may issue tool calls). `None` uses the platform default (50). Must be `1`-`100` |
| `parallel_tool_execution` | `False` | When `True`, multiple tool calls issued in a single LLM turn run concurrently instead of sequentially. Enable only when you know a turn's tool calls are independent (e.g. querying several data sources, or delegating to multiple sub-agents via `AgentTool` in the same turn) |

See [Agent Memory](#agent-memory) for how `memory_config` and the `memory_*_enabled` flags relate — they control two different things.

#### LLMConfig Parameters

The `LLMConfig` class controls how the language model behaves:

```python
from ncp import LLMConfig

config = LLMConfig(
    temperature=0.7,               # 0.0-2.0: Randomness (0=deterministic, 2=very random)
    max_tokens=1500,               # Maximum tokens to generate
    top_p=1.0,                     # 0.0-1.0: Nucleus sampling
    frequency_penalty=0.0,         # -2.0-2.0: Reduce repetition
    presence_penalty=0.0           # -2.0-2.0: Encourage topic diversity
)
```

Model selection itself is platform-managed (configured per-deployment/project), not set on `LLMConfig`.

#### Agent Instructions Best Practices

Write clear, specific instructions:

```python
# Good: Specific and actionable
instructions = """
You are a Python code reviewer. For each code submission:

1. Check for syntax errors and common bugs
2. Verify PEP 8 style compliance
3. Look for security vulnerabilities
4. Suggest performance improvements
5. Rate the code from 1-10 with explanation

Be constructive and educational in your feedback.
"""

# Avoid: Vague instructions
instructions = "You help with code. Be helpful."
```

### Project Structure

Understanding the standard project layout helps organize your agents effectively:

```
my-agent-project/
├── ncp.toml                   # Project configuration
├── requirements.txt           # Python dependencies
├── apt-requirements.txt       # System packages (optional)
├── agents/                    # Agent definitions
│   ├── __init__.py
│   └── main_agent.py          # Primary agent
├── tools/                     # Custom tools
│   ├── __init__.py
│   └── data_tools.py
└── knowledge/                 # Optional: documents for search_knowledge/peek_knowledge
    └── guide.pdf
```

The `knowledge/` directory is optional — if present, the platform ingests its files into a per-agent ChromaDB collection at deploy time, powering the [Knowledge Base Tools](#knowledge-base-tools).

---

## Advanced Features

### MCP Integration

Model Context Protocol (MCP) enables agents to connect to external services and data sources. The NCP SDK supports all MCP transport types via the `MCPConfig` dataclass.

#### Transport Types Overview

```python
from ncp import MCPConfig, TransportType

# Three transport types available:
# 1. stdio            - launch a local process, talk over stdin/stdout
# 2. sse               - Server-Sent Events (URL-based)
# 3. streamable-http   - HTTP streaming (URL-based)
```

#### stdio Transport

For command-line based MCP servers (add the MCP server packages to your requirements.txt):

```python
from ncp import Agent, MCPConfig

filesystem_server = MCPConfig.stdio(
    command="mcp-server-filesystem",
    args=["/path/to/files"],
    env={"DEBUG": "1"},     # optional
    cwd="/path/to/dir",     # optional
)

agent = Agent(
    name="FileAgent",
    description="Agent with filesystem access",
    instructions="Help users manage files and directories",
    mcp_servers=[filesystem_server]
)
```

#### SSE Transport

For URL-based MCP servers using Server-Sent Events:

```python
sse_server = MCPConfig.sse(
    url="https://api.example.com/mcp",
    headers={"Authorization": "Bearer token"}  # optional
)

agent = Agent(
    name="APIAgent",
    description="Agent with API access",
    instructions="Interact with external APIs through MCP",
    mcp_servers=[sse_server]
)
```

#### streamable-http Transport

For HTTP streaming MCP servers:

```python
http_server = MCPConfig.streamable_http(
    url="https://streaming-api.example.com/mcp"
)

streaming_agent = Agent(
    name="StreamingAgent",
    description="Agent with streaming data access",
    instructions="Process real-time data streams",
    mcp_servers=[http_server]
)
```

#### Restricting Exposed Tools with `allowed_tools`

Every `MCPConfig` constructor (and the base `MCPConfig(...)`) accepts an optional `allowed_tools` allowlist. When omitted (`None`), all of the server's tools are exposed; when set, only the listed tool names are exposed to the agent:

```python
sse_server = MCPConfig.sse(
    url="https://api.example.com/mcp",
    allowed_tools=["search", "get_document"],
)
```

#### Multiple MCP Servers

Agents can connect to multiple MCP servers:

```python
from ncp import Agent, MCPConfig

agent = Agent(
    name="MultiServiceAgent",
    description="Agent with multiple external services",
    instructions="""
    You have access to multiple services:
    - Filesystem for file operations
    - Database for data queries
    - API service for external data

    Use the appropriate service based on the user's request.
    """,
    mcp_servers=[
        MCPConfig.stdio(command="mcp-server-filesystem", args=["/data"]),
        MCPConfig.sse(url="https://database-api.example.com/mcp"),
        MCPConfig.streamable_http(url="https://external-api.example.com/stream"),
    ]
)
```

### Data Connectors

Data connectors allow agents to access external data sources (Splunk, ServiceNow, NetBox, Elastic, and more) that are configured in the NCP platform. Simply reference connectors by name — no credentials needed:

```python
from ncp import Agent, tool

@tool
def analyze_logs(query: str) -> dict:
    """Analyze logs from Splunk."""
    # When agent runs on platform, it has access to Splunk-backed tools
    return {"status": "success"}

agent = Agent(
    name="LogAnalyzer",
    description="AI assistant for log analysis",
    instructions="""
    You are a log analysis expert with access to Splunk.
    Help users search logs, identify issues, and generate reports.
    """,
    tools=[analyze_logs],
    connectors=["splunk-prod"]  # Reference by name!
)
```

Connector types and names are configured by platform admins and vary per deployment — the SDK doesn't hardcode a list. Use `ncp connectors list` / `ncp connectors info <name>` (see [Discovering Connectors and Platform Agents](#discovering-connectors-and-platform-agents)) to see what's actually available on your target platform before referencing a connector by name.

#### Multiple Connectors

```python
agent = Agent(
    name="MultiDataAgent",
    description="Agent with access to multiple data sources",
    instructions="You can query Splunk logs and ServiceNow tickets.",
    connectors=["splunk-prod", "servicenow-dev"]
)
```

#### Combining Tools, Connectors, and MCP

```python
from ncp import Agent, tool, MCPConfig

@tool
def custom_analysis(data: dict) -> str:
    """Perform custom analysis on data."""
    return f"Analyzed {len(data)} items"

agent = Agent(
    name="ComprehensiveAgent",
    description="Agent with all tool types",
    instructions="You have access to local tools, data connectors, and external services.",
    tools=[custom_analysis],              # Local Python tools
    connectors=["splunk-prod"],           # Platform data connectors
    mcp_servers=[                         # External MCP servers
        MCPConfig.sse(url="https://api.example.com/mcp")
    ]
)
```

### Agent Memory

The SDK has **two independent memory systems** — don't conflate them.

#### Conversation memory (context window management)

`MemoryConfig` controls how much of the conversation history is kept in the LLM's context window as a chat grows:

```python
from ncp import MemoryConfig, STMStrategy

# Default settings (TOKEN_WINDOW with 25% generation buffer)
config = MemoryConfig()

# Smaller context model with a bigger generation buffer
config = MemoryConfig(stm_config={
    "max_context_tokens": 32768,
    "generation_buffer": 0.30
})

# Fixed turn count instead of a token budget
config = MemoryConfig(
    stm_strategy=STMStrategy.LAST_N_MESSAGES,
    stm_config={"max_messages": 20, "include_tools": True}
)

# Stateless mode — no conversation history, each request independent
config = MemoryConfig(stm_enabled=False)

agent = Agent(..., memory_config=config)
```

The first system message is always preserved regardless of configuration — this is not user-configurable. See `ncp-sdk-examples/memory-config-agent` for a runnable example.

#### Memory store (keeping large results out of context)

The `Memory` class is a Redis-backed reference store you use *inside* `@tool` functions to keep large results out of the LLM's context window — store once, pass a short reference ID around, retrieve/process server-side later. Entries are scoped to the current conversation, expire after 1 hour, accept up to 50MB per entry, and auto-compress above 512KB.

Enable it on the agent with `memory_store_enabled=True` (and optionally `memory_tools_enabled=True` / `memory_context_enabled=True` — see the [Agent reference](#full-agent-reference) above):

```python
from ncp import Agent, Memory, tool

@tool
def store_flow_data(region: str) -> dict:
    """Fetch and store flow records for a region."""
    records = [...]  # large result
    ref = Memory().store(
        records,
        data_type="flows",
        description=f"Flow records for {region}",
    )
    return {"reference_id": ref, "count": len(records)}

@tool
def top_talkers(reference_id: str, top_n: int = 10) -> list:
    """Return top N source IPs by byte count from stored flow data."""
    records = Memory().retrieve(reference_id)
    return sorted(records, key=lambda r: r["bytes"], reverse=True)[:top_n]

@tool
def clear_flow_data(reference_id: str) -> str:
    """Delete a stored flow dataset when it is no longer needed."""
    Memory().delete(reference_id)
    return f"Deleted {reference_id}"

agent = Agent(
    name="FlowAnalyst",
    description="Analyzes network flow data",
    instructions="Fetch and analyze flow data, keeping large datasets out of context.",
    tools=[store_flow_data, top_talkers, clear_flow_data],
    memory_store_enabled=True,
)
```

`Memory` methods:

| Method | Purpose |
|---|---|
| `store(data, data_type, description, connector_scope=None)` | Store JSON-serialisable data, returns an 8-char reference ID |
| `retrieve(reference_id)` | Get back the original (decompressed/deserialized) data |
| `list_entries(data_type=None)` | List lightweight metadata for all entries in the conversation |
| `get_metadata(reference_id)` | Get metadata for one entry without loading the payload |
| `delete(reference_id)` | Delete an entry immediately |

`Memory()` raises `NotImplementedError` when run locally — it only works when the agent is deployed and executing on the platform. See `ncp-sdk-examples/memory-store-agent` for a full worked example (dataset generation, top-talkers analysis, protocol breakdown, lifecycle management).

### Files API

The `Files` client gives an agent direct read access to files uploaded to the platform — project-scoped files and organization-wide admin files. This is direct listing/reading, **not** semantic search; use [Knowledge Base Tools](#knowledge-base-tools) for that.

```python
from ncp import Files, tool

@tool
def list_all_files(file_type: str = None) -> dict:
    """List all files accessible to this agent (project + admin)."""
    files = Files()
    project = files.list_project_files(file_type=file_type)
    admin = files.list_admin_files(file_type=file_type)
    return {"project_files": project, "admin_files": admin}

@tool
def read_config(filename: str) -> str:
    """Read a file's content by name."""
    return Files().read_file_by_name(filename)
```

Key `Files` methods:

| Method | Purpose |
|---|---|
| `list_project_files(file_type=None)` | Files uploaded to the current project (empty list if not in a project context) |
| `list_admin_files(file_type=None, tags=None)` | Organization-wide admin files accessible to all agents |
| `list_all_files(file_type=None)` | Combined project + admin listing, each entry tagged with `source` |
| `get_file_info(file_id, source="project")` | Metadata only, no content |
| `read_file(file_id, source="project")` | Read content by ID |
| `read_file_by_name(filename, source="auto")` | Read content by filename (`auto` searches project then admin) |
| `get_file_path(file_id, source="project")` | Local filesystem path — for archives (`.zip`/`.tar`/`.tar.gz`/`.tgz`/`.gz`) that aren't text; extract yourself with `zipfile`/`tarfile` |
| `get_file_path_by_name(filename, source="auto")` | Same, looked up by filename |

`Files()` raises `NotImplementedError` when run locally — it only works on the platform. See `ncp-sdk-examples/file-agent` for a full worked example.

### Knowledge Base Tools

Two prebuilt `@tool`s ship with the SDK for semantic search over a project's `knowledge/` directory (ingested into a per-agent ChromaDB collection at deploy time):

```python
from ncp import Agent
from ncp.tools.knowledge.search import search_knowledge
from ncp.tools.knowledge.peek import peek_knowledge

agent = Agent(
    name="DocsAssistant",
    description="Answers questions from ingested documentation",
    instructions="Use search_knowledge to find relevant passages before answering.",
    tools=[search_knowledge, peek_knowledge],
)
```

- `search_knowledge(query, n_results=5, filters=None)` — semantic search, with optional metadata filters (e.g. `filters={"file_name": "guide.pdf"}`).
- `peek_knowledge(...)` — sample/metadata overview of the collection, useful for the agent to orient itself before searching.

Like `Files` and `Memory`, these are platform-provided at runtime — calling them outside a deployed agent raises `NotImplementedError`.

### Multi-Agent Composition

Two ways to have one agent delegate to another:

#### Wrap your own `Agent` as a tool with `AgentTool`

```python
from ncp import Agent, AgentTool

math_agent = Agent(
    name="math_expert",
    description="Solves complex math problems",
    instructions="You are a math expert...",
)

math_tool = AgentTool(math_agent)  # or AgentTool(math_agent, name="calculator", description="...")

orchestrator = Agent(
    name="assistant",
    description="General assistant",
    instructions="You help users with various tasks...",
    tools=[math_tool],  # Can delegate to math_agent
)
```

Each execution runs the child agent with fresh context (no shared conversation history) and returns only its final text response.

#### Use a platform-hosted agent as a tool

Platform agents (e.g. `elastic_agent`, `metrics_agent`) already run on the NCP platform and can be composed into your own agent:

```python
# Method 1: direct import — any attribute becomes a proxy for that platform agent name
from ncp.platform.agents import elastic_agent, metrics_agent

# Method 2: factory function
from ncp.platform import agent
elastic = agent("elastic_agent")

from ncp import Agent
my_agent = Agent(
    name="my-network-agent",
    description="Custom agent using platform agents",
    instructions="Use elastic and metrics agents to answer questions.",
    tools=[elastic_agent, metrics_agent],
)
```

Locally/in the playground this proxies to the platform's execute endpoint; after deployment the platform swaps it for a real in-process `AgentTool`. Run `ncp agents list` / `ncp agents info <name>` to see what's available on your target platform (see [Discovering Connectors and Platform Agents](#discovering-connectors-and-platform-agents)). See `ncp-sdk-examples/multi-agent` for a full worked example.

### Calling the LLM from a Tool

`invoke_llm()` lets a tool make an ad-hoc LLM call — useful for summarization, classification, or any sub-task that doesn't need the full agent loop:

```python
from ncp import tool, invoke_llm

@tool
async def summarize(text: str) -> str:
    """Summarize the given text."""
    return await invoke_llm(f"Summarize:\n{text}")

@tool
async def classify(text: str) -> str:
    """Classify text severity using a specific model."""
    return await invoke_llm(
        messages=[
            {"role": "system", "content": "Classify as: critical, high, medium, low"},
            {"role": "user", "content": text},
        ],
        model_name="gpt-4o",
        temperature=0.0,
    )
```

Provide either `prompt` or `messages`, not both. `model_name`, `temperature`, and `max_tokens` are optional overrides; omitting `model_name` uses the project/platform default. This only works when the tool is actually executing on the platform — calling it elsewhere raises `RuntimeError`.

### Generic SSH/HTTP Connectors

For custom tools that need to talk to a registered generic (SSH or HTTPS) connector — one call, or several against the same host reusing one connection — without handling connection setup, RBAC, or credentials yourself:

```python
from ncp import tool
from ncp.connections import ssh_session

@tool
async def check_switch_health(connector_name: str = None) -> dict:
    """Check basic health of a network device over SSH."""
    try:
        async with ssh_session(connector_name=connector_name) as session:
            version = await session.run("show version")
            interfaces = await session.run("show interfaces status")
            return {"version": version["stdout"], "interfaces": interfaces["stdout"]}
    except ValueError as e:
        return {"error": str(e)}
```

`http_session` works the same way for HTTPS connectors, exposing `request(method, path)` instead of `run(command)`. Both:

- Resolve the named connector, enforce RBAC, and decrypt/apply stored credentials entirely in-process — no raw credential ever reaches your tool code.
- Leave `connector_name` optional and LLM-facing on your own tool's signature (resolved per call, or auto-resolved when there's only one candidate connector) rather than hardcoded by you.
- Raise `ValueError` on resolution/RBAC/connect failure — wrap the call in `try/except ValueError` to turn that into a structured error result.
- Never raise for a per-command/per-request failure — a non-zero exit status or non-2xx response is just data, returned normally as `{"stdout", "stderr", "exit_status"}` or `{"body", ...}`.

See `ncp-sdk-examples/cisco-switch-agent` for a full worked example.

### Metrics API

`Metrics` queries device/interface/link/hardware telemetry and time-series data (CPU, memory, interface counters, syslog, reboot/OS-change/flap events, flow data) from the platform's collector database:

```python
from ncp import Metrics, tool

@tool
def check_cpu(hostname: str) -> dict:
    """Check CPU utilization for a device."""
    metrics = Metrics()
    cpu = metrics.get_cpu_utilization(hostname=hostname, hours=1)
    return cpu[0] if cpu else {"error": "No data"}
```

Like `Files`/`Memory`, `Metrics()` only works when deployed on the platform. See `ncp-sdk-examples/metrics-basics-agent` for a full worked example covering the available query methods.

---

## Dependency Management

### Python Dependencies

#### requirements.txt

List all Python packages your agent needs:

```txt
pandas>=1.5.0
numpy>=1.21.0
requests>=2.28.0
```

#### Version Pinning Strategies

```txt
# Exact versions (most restrictive)
requests==2.28.2
pandas==1.5.3
```

### System Dependencies

#### apt-requirements.txt

Specify system packages needed by your agent:

```txt
# Basic utilities
curl
wget
git
```

#### Managing Dependencies in Development

```bash
# Create requirements.txt from current environment
pip freeze > requirements.txt

# Install from requirements.txt
pip install -r requirements.txt

# Install in editable mode for development
pip install -e .

# Check for security vulnerabilities
pip install safety
safety check -r requirements.txt
```

---

## SDK Workflow

### Project Initialization

#### Creating a New Project

```bash
# Basic project initialization
ncp init my-agent-project

# Navigate to project directory
cd my-agent-project

# Project structure created:
# ├── ncp.toml
# ├── requirements.txt
# ├── apt-requirements.txt
# ├── agents/
# │   └── main_agent.py
# └── tools/
#     └── __init__.py
```

#### Post-Initialization Setup

```bash
cd my-agent-project

# Verify setup
ncp validate .
```

### Development

#### Development Best Practices

1. **Start Simple**: Begin with basic tools and gradually add complexity
2. **Test Locally**: Test tool logic before integrating with agents
3. **Use Type Hints**: Leverage Python type hints for better validation
4. **Document Everything**: Write clear docstrings for tools and agents
5. **Handle Errors**: Implement proper error handling in tools

### Validation

```bash
ncp validate .
ncp validate /path/to/project
```

Checks project structure, `ncp.toml` validity, agent definitions, dependency declarations, and that all imports resolve.

### Packaging

```bash
# Package the current project
ncp package .

# Output: my-agent-project.ncp (created in current directory)

# Package with custom output name
ncp package . --output my-custom-agent.ncp

# Tag with a version
ncp package . --version 1.0.0

# Always validate before packaging
ncp validate . && ncp package .
```

### Deployment

#### Authentication

Before deploying, authenticate with your NCP platform to store credentials. This can be run from anywhere — no project directory required:

```bash
ncp authenticate
ncp authenticate --platform https://ncp.example.com
```

This stores your credentials in `~/.ncp/credentials.toml` (per-user, not per-project), so you don't need to pass `--platform` and `--api-key` flags with every command, and won't need to re-authenticate when working on other agent projects targeting the same platform.

If you authenticate against multiple NCP instances (e.g. dev and prod), each platform's credentials are kept separately; the most recently authenticated platform becomes the default used when `--platform` isn't specified.

**Note:** For backward compatibility, a project's `ncp.toml` can still have a `[platform]` section as a per-project override (it takes precedence over the home-directory store for that project). This is no longer written by `ncp authenticate`, but if you add one manually, do not commit it to version control:

```bash
# In your .gitignore
ncp.toml
```

#### Platform Deployment

```bash
# Package your project
ncp package .

# Deploy using stored credentials
ncp deploy my-agent-project.ncp

# Or with explicit credentials (overrides stored ones)
ncp deploy my-agent-project.ncp --platform https://ncp.example.com --api-key your-key

# Update an existing deployment
ncp deploy my-agent-project.ncp --update my-agent
```

#### Interactive Playground

Test your agent interactively, similar to `ollama run`:

```bash
# From your project directory (uses stored credentials)
ncp playground

# Specify an agent by name
ncp playground --agent my-agent

# Test a packaged agent
ncp playground --agent my-agent.ncp

# Show tool calls/results, or logs, during the session
ncp playground --agent my-agent --show-tools
ncp playground --logs             # INFO level
ncp playground --logs DEBUG
```

**Playground Features:**

- **Interactive Chat**: Chat with your agent in real-time
- **Special Commands**: `/help`, `/exit`, `/reset` (clear conversation history), `/clear` (clear screen)
- **Conversation History**: Maintains context across messages within the session

**Example Session:**

```
$ ncp playground
🎮 NCP Agent Playground

📁 Project: my-agent
🌐 Platform: https://ncp.example.com

────────────────────────────────────────────────────────────

💬 Interactive Chat Mode (Ctrl+C to exit)
   Type your message and press Enter to send

You> Hello! Can you help me analyze network devices?

Agent> Hello! I'd be happy to help you analyze network devices.
       I have access to tools for pinging devices, checking interface
       status, and backing up configurations. What would you like to do?

You> /exit
👋 Goodbye!
```

#### Post-Deployment Management

```bash
# List deployed agents (uses stored credentials)
ncp list
ncp list --platform https://ncp.example.com

# Remove a deployed agent
ncp remove --agent my-agent
```

All commands support `--platform` and `--api-key` flags to override stored credentials.

### Testing Deployed Agents

For scripted, non-interactive testing — e.g. a build → deploy → test → iterate loop — use `ncp ask` or the underlying `ncp.testing` Python API instead of the interactive playground.

#### CLI: `ncp ask`

```bash
ncp ask "How many devices are in NetBox?" --agent my-agent
ncp ask "list your tools" --agent my-agent --json
ncp ask "..." --agent my-agent --timeout 60
```

Prints the answer, the tools the agent called, and any error. Exit code is `0` on success and `1` on failure, so it composes cleanly in scripts and test loops.

#### Python: `ncp.testing`

```python
from ncp.testing import ask_agent

result = ask_agent("How many devices are in NetBox?", agent="my-agent")
print(result.answer)        # the agent's text response
print(result.tools)         # e.g. ['netbox_get_objects']
print(result.tool_errors)   # any tool-level errors
print(result.ok)            # True when there's no transport or tool error
```

`ask_agent()` is a synchronous wrapper around `query_agent_async()`; both connect to the deployed agent over the same playground WebSocket the CLI uses, using stored credentials (or explicit `platform`/`api_key` args) and a configurable `timeout` (seconds, default 120).

### Discovering Connectors and Platform Agents

```bash
# Data connectors configured on the platform
ncp connectors list
ncp connectors info NetboxEngg
ncp connectors info NetboxEngg --json

# Pre-built platform agents (elastic_agent, metrics_agent, etc.) usable via
# ncp.platform / ncp.platform.agents — see Multi-Agent Composition above
ncp agents list
ncp agents info elastic_agent
```

`ncp connectors info` and `ncp agents info` show full details (tools, descriptions, requirements) for one connector/agent so you can decide how to reference it from your own agent code.

### Custom Agent Onboarding (Admin-only)

`ncp onboard` is distinct from `ncp deploy`. Deployed agents run containerized; **onboarded** agents run integrated within the platform process (no container isolation) and are made available to users based on role-based `CustomAgents` permissions in Admin > Roles & Permissions. This requires admin privileges on the target platform.

```bash
ncp onboard my-agent.ncp
ncp onboard my-agent.ncp --update
ncp onboard-list
ncp onboard-remove --agent my-custom-agent
```

---

## Examples

Full, runnable example projects live in the companion [`ncp-sdk-examples`](https://github.com/AvizNetworks/ncp-sdk-examples) repository:

| Example | Demonstrates |
|---|---|
| `hello-agent` | Minimal agent — the fastest way to see the SDK work end to end |
| `weather-agent` | Basic tool + agent, sync tools |
| `calculator-agent` | Multiple tools on one agent |
| `async-tools-agent` | Async `@tool` functions |
| `multi-agent` | `AgentTool` and `ncp.platform` composition |
| `metrics-basics-agent` | The `Metrics` API |
| `memory-config-agent` | Conversation memory via `MemoryConfig`/`STMStrategy` |
| `memory-store-agent` | The `Memory` reference-store pattern (store/retrieve/list/delete) |
| `file-agent` | The `Files` API (project + admin files) |
| `cisco-switch-agent` | Generic connectors via `ssh_session` |
| `splunk-connector-agent` | Data connectors (`connectors=[...]`) |

```bash
git clone https://github.com/AvizNetworks/ncp-sdk-examples
cd ncp-sdk-examples/<example-name>
pip install -r requirements.txt
ncp validate .
ncp authenticate
ncp package .
ncp deploy <example-name>.ncp
ncp playground --agent <example-name>
```

---
