Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ kbagent config new --component-id ID [--name NAME] [--project NAME] [--output-di

kbagent encrypt values --project ALIAS --component-id ID --input JSON|@file|- [--output-file PATH]

kbagent kai ping [--project NAME]
kbagent kai ask --message "question" [--project NAME]
kbagent kai chat --message "msg" [--chat-id ID] [--project NAME]
kbagent kai history [--project NAME] [--limit N]

kbagent context
kbagent init [--from-global]
kbagent doctor [--fix]
Expand Down
10 changes: 10 additions & 0 deletions docs/e2e-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ Steps 21-27 are wrapped in try/except -- if workspace API is unavailable on the
|-----:|---------|------------------|
| 37 | `sharing list`, `lineage show` | Both return valid responses (may be empty on single project) |

### Phase 12.5: Kai (Keboola AI Assistant)

| Step | Command | What is verified |
|-----:|---------|------------------|
| 38 | `kai ping` | Server health, timestamp, MCP status. Gracefully skips all kai tests if `agent-chat` feature not enabled |
| 38 | `kai ask -m "..."` | One-shot question, verify response text + chat_id. Skips if auth fails (token type) |
| 38 | `kai history --limit 5` | At least 1 chat after asking |

Steps are wrapped in graceful skip logic — if Kai is not available (feature flag or auth), remaining kai tests are skipped without failing.

### Phase 13: Job commands

| Step | Command | What is verified |
Expand Down
4 changes: 4 additions & 0 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ If kbagent is not installed or you need the full standalone reference, run `kbag
| Link a shared bucket into a project | `kbagent sharing link --project PROJECT --source-project-id SOURCE-PROJECT-ID --bucket-id BUCKET-ID` |
| Remove a linked bucket from a project | `kbagent sharing unlink --project PROJECT --bucket-id BUCKET-ID` |
| Show cross-project data lineage via bucket sharing | `kbagent lineage show` |
| Check Kai server health and MCP connection status | `kbagent kai ping` |
| Ask Kai a one-shot question and get the full response | `kbagent kai ask --message MESSAGE` |
| Send a message to Kai in a chat session | `kbagent kai chat --message MESSAGE` |
| List recent Kai chat sessions | `kbagent kai history` |
| List development branches from connected projects | `kbagent branch list` |
| Create a new development branch and auto-activate it | `kbagent branch create --project PROJECT --name NAME` |
| Set an existing development branch as active | `kbagent branch use --project PROJECT --branch BRANCH` |
Expand Down
72 changes: 72 additions & 0 deletions plugins/kbagent/skills/kbagent/references/kai-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Kai (Keboola AI Assistant) Workflow

Kai is Keboola's cloud AI assistant with MCP access to project data.
kbagent bridges Claude Code (local) to Kai (cloud) for Keboola-specific questions.

> **BETA**: Kai commands require a project with the `agent-chat` feature enabled.
> Token authentication requirements are being finalized.

## When to use Kai vs local tools

| Situation | Use |
|-----------|-----|
| Need project-specific context (tables, configs, lineage) | `kbagent kai ask` |
| Simple data listing (buckets, tables, configs) | `kbagent config list`, `kbagent storage tables` |
| Need Keboola domain knowledge (component behavior, best practices) | `kbagent kai ask` |
| Need to modify data (upload, create, delete) | Direct CLI commands |

## Quick start

```bash
# Check if Kai is available
kbagent kai ping --project my-project

# Ask a question about the project
kbagent kai ask --project my-project -m "What tables do I have?"

# Multi-turn conversation
kbagent kai chat --project my-project -m "Help me debug my pipeline"
# Note the chat_id in the response, then continue:
kbagent kai chat --project my-project --chat-id CHAT_ID -m "What about the error in step 3?"

# View chat history
kbagent kai history --project my-project --limit 10
```

## Feature detection

Kai requires the `agent-chat` feature flag on the project. If not enabled,
kai commands return error code `KAI_NOT_ENABLED` with a clear message.

Check via: `kbagent --json kai ping --project ALIAS` — exit code 0 means Kai is available.

## JSON output

All kai commands support `--json` for structured output:

```bash
# Ping
kbagent --json kai ping --project my-project
# {"status": "ok", "data": {"timestamp": "...", "mcp_status": "ok", ...}}

# Ask
kbagent --json kai ask --project my-project -m "How many tables?"
# {"status": "ok", "data": {"chat_id": "uuid", "response": "You have 19 tables."}}

# History
kbagent --json kai history --project my-project
# {"status": "ok", "data": {"chats": [...], "has_more": false}}
```

## Common patterns for Claude Code

```bash
# Use kai ask as a Keboola knowledge oracle
kbagent --json kai ask --project prod -m "Is it safe to drop bucket in.c-legacy?"

# Get project overview for onboarding
kbagent --json kai ask --project prod -m "Describe the data flow in this project"

# Debug a failed job
kbagent --json kai ask --project prod -m "Why did job 12345 fail?"
```
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies = [
"pyyaml>=6",
"packaging>=23",
"prompt-toolkit>=3.0",
"kai-client>=0.11.0",
]

[project.scripts]
Expand Down
5 changes: 5 additions & 0 deletions src/keboola_agent_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .commands.encrypt import encrypt_app
from .commands.init import init_command
from .commands.job import job_app
from .commands.kai import kai_app
from .commands.lineage import lineage_app
from .commands.org import org_app
from .commands.permissions import permissions_app
Expand All @@ -37,6 +38,7 @@
from .services.doctor_service import DoctorService
from .services.encrypt_service import EncryptService
from .services.job_service import JobService
from .services.kai_service import KaiService
from .services.lineage_service import LineageService
from .services.mcp_service import McpService
from .services.org_service import OrgService
Expand Down Expand Up @@ -77,6 +79,7 @@
app.add_typer(storage_app, name="storage", rich_help_panel=_BROWSE)
app.add_typer(sharing_app, name="sharing", rich_help_panel=_BROWSE)
app.add_typer(lineage_app, name="lineage", rich_help_panel=_BROWSE)
app.add_typer(kai_app, name="kai", rich_help_panel=_BROWSE)

# -- Development --
_DEV = "Development"
Expand Down Expand Up @@ -186,6 +189,7 @@ def main(
sync_service = SyncService(config_store=config_store)
encrypt_service = EncryptService(config_store=config_store)
workspace_service = WorkspaceService(config_store=config_store)
kai_service = KaiService(config_store=config_store)
doctor_service = DoctorService(config_store=config_store, mcp_service=mcp_service)
version_service = VersionService()

Expand Down Expand Up @@ -224,6 +228,7 @@ def main(
ctx.obj["sync_service"] = sync_service
ctx.obj["encrypt_service"] = encrypt_service
ctx.obj["workspace_service"] = workspace_service
ctx.obj["kai_service"] = kai_service
ctx.obj["doctor_service"] = doctor_service
ctx.obj["version_service"] = version_service

Expand Down
1 change: 1 addition & 0 deletions src/keboola_agent_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ def verify_token(self) -> TokenVerifyResponse:
project_name=owner.get("name", ""),
owner_name=owner.get("name", ""),
default_backend=owner.get("defaultBackend", "snowflake"),
features=owner.get("features", []),
)

def list_components(
Expand Down
18 changes: 18 additions & 0 deletions src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,24 @@
--input accepts: inline JSON, @file.json (from file), or - (from stdin).
--branch is a CLI flag (NOT a tool input param). Do not pass branch_id in --input.

### Kai -- Keboola AI Assistant (BETA)

kbagent kai ping [--project NAME]
Check Kai server health and MCP connection status.
Fails with KAI_NOT_ENABLED if the project lacks the 'agent-chat' feature.

kbagent kai ask --message "question" [--project NAME]
One-shot question to Kai. Collects full response. Use --json for structured output.
Kai has MCP access to project data -- use for Keboola-specific questions
(e.g. "What tables do I have?", "Is it safe to drop bucket X?").

kbagent kai chat --message "msg" [--chat-id ID] [--project NAME]
Send message in a chat session. Use --chat-id to continue a conversation.
Without --chat-id starts a new chat. Returns chat_id for continuation.

kbagent kai history [--project NAME] [--limit N]
List recent Kai chat sessions. Default limit: 10.

### Utility Commands

kbagent init [--from-global]
Expand Down
188 changes: 188 additions & 0 deletions src/keboola_agent_cli/commands/kai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""CLI commands for Kai (Keboola AI Assistant) integration.

Bridges Claude Code (local) to Kai (cloud) via kbagent CLI.
Kai has MCP access to project data and can answer Keboola-specific questions.
"""

import typer

from ..errors import ConfigError, KeboolaApiError
from ._helpers import check_cli_permission, get_formatter, get_service, map_error_to_exit_code

kai_app = typer.Typer(help="(BETA) Keboola AI Assistant (Kai) — ask questions about your project")


@kai_app.callback(invoke_without_command=True)
def _kai_permission_check(ctx: typer.Context) -> None:
check_cli_permission(ctx, "kai")


@kai_app.command("ping")
def kai_ping(
ctx: typer.Context,
project: str | None = typer.Option(
None,
"--project",
help="Project alias (uses default if omitted).",
),
) -> None:
"""Check Kai server health and MCP connection status."""
formatter = get_formatter(ctx)
service = get_service(ctx, "kai_service")

try:
alias = service.resolve_alias(project)
result = service.ping(alias)
except ConfigError as exc:
formatter.error(message=exc.message, error_code="CONFIG_ERROR")
raise typer.Exit(code=5) from None
except KeboolaApiError as exc:
formatter.error(message=exc.message, error_code=exc.error_code)
raise typer.Exit(code=map_error_to_exit_code(exc)) from None

def _human(console, data):
console.print(f"[bold green]Kai is alive[/bold green] ({data['project_alias']})")
console.print(f" Timestamp: {data['timestamp']}")
console.print(f" App: {data['app_name']} {data['app_version']}")
console.print(f" Server: {data['server_version']}")
console.print(f" MCP connection: {data['mcp_status']}")

formatter.output(result, _human)


@kai_app.command("ask")
def kai_ask(
ctx: typer.Context,
message: str = typer.Option(
...,
"--message",
"-m",
help="Question to ask Kai about your project.",
),
project: str | None = typer.Option(
None,
"--project",
help="Project alias (uses default if omitted).",
),
) -> None:
"""Ask Kai a one-shot question and get the full response.

Kai has access to your project's data, configurations, and lineage
via MCP tools. Use this for Keboola-specific questions that require
project context.
"""
formatter = get_formatter(ctx)
service = get_service(ctx, "kai_service")

try:
alias = service.resolve_alias(project)
result = service.ask(alias, message)
except ConfigError as exc:
formatter.error(message=exc.message, error_code="CONFIG_ERROR")
raise typer.Exit(code=5) from None
except KeboolaApiError as exc:
formatter.error(message=exc.message, error_code=exc.error_code)
raise typer.Exit(code=map_error_to_exit_code(exc)) from None

def _human(console, data):
console.print(data["response"])

formatter.output(result, _human)


@kai_app.command("chat")
def kai_chat(
ctx: typer.Context,
message: str = typer.Option(
...,
"--message",
"-m",
help="Message to send to Kai.",
),
chat_id: str | None = typer.Option(
None,
"--chat-id",
help="Continue an existing chat session.",
),
project: str | None = typer.Option(
None,
"--project",
help="Project alias (uses default if omitted).",
),
) -> None:
"""Send a message to Kai in a chat session.

Use --chat-id to continue a previous conversation.
Without --chat-id, starts a new chat.
"""
formatter = get_formatter(ctx)
service = get_service(ctx, "kai_service")

try:
alias = service.resolve_alias(project)
result = service.chat_message(alias, message, chat_id=chat_id)
except ConfigError as exc:
formatter.error(message=exc.message, error_code="CONFIG_ERROR")
raise typer.Exit(code=5) from None
except KeboolaApiError as exc:
formatter.error(message=exc.message, error_code=exc.error_code)
raise typer.Exit(code=map_error_to_exit_code(exc)) from None

def _human(console, data):
console.print(data["response"])
console.print(f"\n[dim]Chat ID: {data['chat_id']}[/dim]")

formatter.output(result, _human)


@kai_app.command("history")
def kai_history(
ctx: typer.Context,
project: str | None = typer.Option(
None,
"--project",
help="Project alias (uses default if omitted).",
),
limit: int = typer.Option(
10,
"--limit",
"-n",
help="Maximum number of chats to return.",
),
) -> None:
"""List recent Kai chat sessions."""
formatter = get_formatter(ctx)
service = get_service(ctx, "kai_service")

try:
alias = service.resolve_alias(project)
result = service.get_history(alias, limit=limit)
except ConfigError as exc:
formatter.error(message=exc.message, error_code="CONFIG_ERROR")
raise typer.Exit(code=5) from None
except KeboolaApiError as exc:
formatter.error(message=exc.message, error_code=exc.error_code)
raise typer.Exit(code=map_error_to_exit_code(exc)) from None

def _human(console, data):
chats = data["chats"]
if not chats:
console.print("[dim]No chat history.[/dim]")
return
from rich.table import Table

table = Table(title=f"Kai Chat History ({data['project_alias']})")
table.add_column("Chat ID", style="cyan", no_wrap=True)
table.add_column("Title")
table.add_column("Created", style="dim")
for chat in chats:
table.add_row(
chat["id"][:12] + "...",
chat["title"],
chat["created_at"] or "",
)
console.print(table)
if data["has_more"]:
console.print("[dim]More chats available. Use --limit to see more.[/dim]")

formatter.output(result, _human)
5 changes: 5 additions & 0 deletions src/keboola_agent_cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@

# --- AI Service ---
AI_SERVICE_TIMEOUT: httpx.Timeout = httpx.Timeout(connect=5.0, read=15.0, write=5.0, pool=5.0)

# --- Kai (Keboola AI Assistant) ---
KAI_FEATURE_FLAG: str = "agent-chat"
KAI_REQUEST_TIMEOUT: float = 300.0 # 5 min for non-streaming requests
KAI_STREAM_TIMEOUT: float = 600.0 # 10 min for SSE streaming responses
SECRET_PLACEHOLDER: str = "<YOUR_SECRET>"

# --- Job Run ---
Expand Down
Loading