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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.41.1",
"version": "0.41.9",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.41.1",
"version": "0.41.9",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
4 changes: 4 additions & 0 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ a critical failure.
AI-agent run timeline persistence (cost / token / per-tool summary
on every persisted `AgentRun` plus `GET /agents/{id}/runs/{run_id}/events`
for replay) needs 0.40.0+,
`POST /ai/chat/stream` (generic Local AI co-pilot chat backed by the
user's local claude / codex / gemini CLI; backs the dashboard
Local AI tile that replaces Kai for non-master-token projects)
needs 0.41.9+,
`storage retype` is a future composite), you
MUST refuse the task and return a handoff message to the parent:
`"Cannot proceed safely on kbagent <version>. Missing: <commands>.
Expand Down
60 changes: 60 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
# Gotchas -- Response Parsing and Common Pitfalls

## Web UI `Kai Chat` is gone — replaced by `Local AI` (since v0.41.9)

The web UI dashboard tile / left-nav entry previously labelled **Kai
Chat** has been replaced by **Local AI** (PR #301, follow-up to #291
closed-wontfix and #288 closed-wontfix). The new tile is backed by
`POST /ai/chat/stream`, a third instance of the same stateless-helper
pattern as `POST /agents/prompt/improve/stream` and
`POST /workspaces/sql/improve/stream`. It spawns the user's local
`claude` / `codex` / `gemini` CLI with a meta-prompt grounding it as
a kbagent co-pilot.

**Why the swap:**

- Kai requires a **master** Storage API token. `kbagent org setup`
generates non-master tokens by default for security reasons, so any
project registered via that path had its Kai tile broken.
- Kai is per-project; cross-project work (lineage, migration assistant,
multi-project comparison) was structurally impossible inside Kai.
- The local AI uses any Storage token kbagent already has AND handles
multi-project flags natively (`--project NAME`).

**What stays:**

- `POST /kai/chat` and the rest of the `/kai/*` backend endpoints
remain available for HTTP callers that explicitly want Kai's
per-project session-state API. Only the dashboard UI tile + left
nav entry was swapped. `kbagent kai ping|preflight|ask|chat`
CLI commands are unchanged.

**Implication for AI agents:**

- If your script targets the web UI (e.g. screen-scraping or Playwright
automation), the page id changed from `kai` to `localai` in
`UIState.page` and the route from `KaiPage` to `LocalAiPage`. The
endpoint flipped from `POST /kai/chat` (blocking JSON) to
`POST /ai/chat/stream` (SSE) -- different wire protocol, different
envelope.

## Dashboard `▶ run` button on scheduled agents uses BLOCKING `/agents/{id}/run`, NOT the SSE stream (since v0.41.9)

The dashboard's Scheduled agents tile gained an inline `▶ run` button
per row (issue #292). It fires `POST /agents/{task_id}/run` -- the
blocking variant -- and invalidates the `['agents']` query cache on
completion so the row's `last_run_at` + status pill refresh inline.

The Agents PAGE (`/agents`) uses a different code path: when its `▶`
button fires, it opens the Run drawer that streams via
`POST /agents/{task_id}/run/stream` (SSE with late-attach support).

**Pick the right endpoint:**

- Need live tool_use / token-cost / `stream-json` events as they
arrive? Use `/agents/{id}/run/stream`.
- Just need "fire and forget; tell me when it's done; let me move on"?
Use `/agents/{id}/run`. This is what the dashboard tile uses.

Both endpoints persist the same `AgentRun` record on disk; the blocking
endpoint returns it once the run completes, the SSE endpoint streams
events and emits a final `done` SSE frame mirroring the same record.

## Semantic-layer constraint `rule` is a STRING, not an object (since v0.41.0)

- The `sl-builder` skill docs (in `04_AI_Kit/ai-kit/`) describe range
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-agent-cli"
version = "0.41.1"
version = "0.41.9"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
6 changes: 6 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.41.9": [
"Fix: Workspace detail Drawer scrim (#286). The earlier 90% opacity (`bg-zinc-950/90`) read as a broken layout -- Vojta reported 'the left half of the screen looks crashed'. The original concern (clickthrough on agent-task buttons beneath the scrim) was misdiagnosed: the click-catch `<div className='flex-1' onClick={onClose}>` blocks pointer events at the layout level regardless of scrim opacity, so the dimming is purely visual. New scrim: `bg-zinc-900/50` in light mode + `bg-black/70` in dark mode, both retain `backdrop-blur-sm`. Restores the 'I opened a modal on top' depth cue without breaking modality.",
"New: Dashboard 'Scheduled agents' tile gets an inline `run` button per row (#292). The Agents page already had it; the dashboard tile didn't. New `ScheduledAgentRow` component extracted from the inline list; the button fires `POST /agents/{id}/run` (blocking variant) and invalidates the `['agents']` and `['agent-runs', id]` query keys so `last_run_at` + status pill flip live without a manual reload. Deliberate choice of blocking-vs-SSE: the tile is a glance-and-move-on surface, not a live progress viewer -- users who want to watch tool_use events use the full Run drawer on the Agents page.",
"New: Dashboard 'Local AI' tile replaces the per-project 'Kai Chat' (#300, follow-up to #291 wontfix). New page at `/localai` is a generic chat surface backed by the user's local `claude` / `codex` / `gemini` CLI -- same `stream_ai_agent_events` machinery as the workspace SQL helper and agent prompt helper, built as the third instance of the same stateless-helper layer (`/agents/prompt/improve/stream` + `/workspaces/sql/improve/stream` + `/ai/chat/stream`). Why: Kai requires a master Storage token to work, and `kbagent org setup` produces non-master tokens by default for security reasons (#291 closed wontfix). The Local AI tile works against any Storage token kbagent already has, and -- unlike Kai -- handles cross-project work natively via `--project NAME` flags. The dashboard hero 'Ask <cli>' input drops the typed message into `UIState.pendingLocalAiMessage` and navigates to `/localai`, which auto-fires the request on mount; full chat plumbing (CLI selector, project picker, transparency panels for the meta-prompt + tool_use activity log, markdown rendering with code blocks, abort button) lives on the dedicated page. The `build_local_ai_meta_prompt` builder is the most generic of the three helper meta-prompts: no output-shape constraint (chat renders markdown verbatim, no fence-stripping needed), no single-task framing -- just 'you are running inside kbagent serve, here is the user's question, run real commands to answer'. The kbagent skill (~70 KB of docs) is NOT inlined into every request; the AI is told to run `kbagent context` on demand instead, mirroring how Claude Code's plugin loader bootstraps the skill. Kai backend endpoints (`/kai/*`) remain available for callers that explicitly want Kai's per-project chat with API session state; only the dashboard tile / left-nav entry was swapped. The old `pages/Kai.tsx` is deleted; the new `pages/LocalAi.tsx` is feature-equivalent (single-shot for v1, multi-turn history forwarded into the prompt is the v2 follow-up). 15 unit tests cover the meta-prompt content (user message verbatim, project / branch hints, serve-URL fast-path, no output-shape contract, markdown contract) and SSE endpoint integration (init carries meta_prompt, done event flows through unmodified -- no SQL-style post-processing, error path surfaces as `done` with `status: error`).",
"Plugin docs: `plugins/kbagent/skills/kbagent/references/gotchas.md` gains `(since v0.41.9)` notes for the Local AI tile replacement (Kai backend stays but the nav entry moves) and the dashboard 'Run' button vs Agents page 'Run' button (the dashboard uses blocking `POST /agents/{id}/run`; the Agents page uses SSE `POST /agents/{id}/run/stream` with live progress + late-attach -- pick the right one for the UX). No new CLI commands; `CLAUDE.md ## All CLI Commands` unchanged.",
],
"0.41.1": [
"Fix: startup auto-update hook now preserves the optional `[server]` extras. Before this release, `kbagent serve --ui` could trigger an auto-update that ran a bare `uv tool install --upgrade git+...` (no `--with` flag), silently dropping the FastAPI + uvicorn extras a user originally installed with `--with 'keboola-agent-cli[server]'`. The next line of the same boot would then refuse to start with `ModuleNotFoundError: No module named 'fastapi'`. The fix in v0.40.2 only patched the explicit `kbagent update` command (`version_service._update_kbagent`); the startup hook in `auto_update._perform_update` was left running the old bare command. Now both paths delegate to a shared `build_kbagent_upgrade_command()` helper that probes `importlib.util.find_spec('fastapi')` and pairs `--with 'keboola-agent-cli[server]'` with `--force` when extras are detected. Two new tests pin the behavior in both directions (extras -> `--force --with`, no-extras -> plain `--upgrade`).",
"Fix: `kbagent version` now persists the freshly-fetched `latest_version` (and MCP version + install method) to the auto-update cache. Before this release, `get_versions()` made a live GitHub round-trip but did NOT write the result back to `~/.config/keboola-agent-cli/version_cache.json`. The 1-hour TTL'd cache stayed pinned to whatever value the auto-update hook last wrote -- so `kbagent version` would correctly show `v0.41.0 available` while a follow-up `kbagent serve --ui` on the same machine still auto-updated to whatever stale version the cache held (e.g. 0.40.3). Combined with the extras-drop bug above, this produced the worst-case scenario reported by users: `kbagent version` says new release available, `kbagent serve --ui` upgrades to a different older release and breaks. Now `get_versions()` writes the cache (lazy-imported to avoid a circular import) at the end of every successful fetch; write failures are caught and logged at debug level so the version command never crashes on a read-only HOME / disk-full / permission edge case.",
Expand Down
17 changes: 17 additions & 0 deletions src/keboola_agent_cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@
# short enough to fail fast on a dead serve.
HTTP_DEFAULT_TIMEOUT: float = 60.0

# --- AI helper subprocess timeouts (seconds) ---
# Each of the three stateless AI helpers spawns a local claude / codex /
# gemini CLI via stream_ai_agent_events; these caps protect against a stuck
# CLI camping on the SSE connection. Centralised here so they stay
# governable from one place instead of three router files.
#
# - Prompt helper: rewrite a draft prompt for a scheduled agent. Short
# single-turn task, no tool calls expected -> 180s.
# - SQL helper: write workspace SQL. May round-trip through
# `kbagent storage bucket-detail` + INFORMATION_SCHEMA queries -> 180s.
# - Chat helper: generic Local AI co-pilot. Can involve several tool
# invocations + a longer summary -> 300s, matching the longest
# credible single-turn budget without leaving stuck connections open.
AI_PROMPT_HELPER_TIMEOUT: float = 180.0
AI_SQL_HELPER_TIMEOUT: float = 180.0
AI_CHAT_HELPER_TIMEOUT: float = 300.0

# --- Version Check ---
VERSION_CHECK_TIMEOUT: float = 4.0 # seconds for fetching latest version from remote
MCP_PYPI_URL: str = "https://pypi.org/pypi/keboola-mcp-server/json"
Expand Down
3 changes: 3 additions & 0 deletions src/keboola_agent_cli/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from .dependencies import ServiceRegistry, install_registry
from .routers import (
agents,
ai_chat,
branches,
components,
configs,
Expand Down Expand Up @@ -220,6 +221,7 @@ async def _generic_handler(_request, exc: Exception):
app.include_router(data_apps.router)
app.include_router(mcp.router)
app.include_router(kai.router)
app.include_router(ai_chat.router)
app.include_router(encrypt.router)
app.include_router(search.router)
app.include_router(semantic_layer.router)
Expand Down Expand Up @@ -382,6 +384,7 @@ def _is_ui_public(method: str, path: str) -> bool:
"/data-apps",
"/mcp",
"/kai",
"/ai",
"/encrypt",
"/search",
"/semantic-layer",
Expand Down
94 changes: 94 additions & 0 deletions src/keboola_agent_cli/server/agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,100 @@ def clean_prompt_helper_response(text: str) -> str:
return text.strip()


def build_local_ai_meta_prompt(
*,
message: str,
project: str | None = None,
branch_id: int | None = None,
serve_url: str | None = None,
) -> str:
"""Compose the meta-prompt for the dashboard Local AI chat (#300).

This is the most generic of the three helper meta-prompts in this
module: it does NOT pin an output shape (unlike SQL helper which
must emit raw SQL) and does NOT pin a task shape (unlike the agent
prompt helper which rewrites a draft). It simply tells the AI:
"you are running inside kbagent serve, you have the kbagent CLI on
PATH, here is what the user wants — answer it."

The user's local Claude / codex / gemini install handles markdown
rendering on the UI side, so the prompt encourages markdown output
rather than the spartan output contract that the SQL / prompt
helpers enforce.

The kbagent-skill content (workflow knowledge, gotchas, command
reference) is NOT inlined verbatim — it is ~70 KB of documentation
that would balloon every chat round trip. Instead the AI is told
to run ``kbagent context`` to load the full documentation on demand,
mirroring how Claude Code's plugin loader bootstraps the skill.
"""
message_clean = message.strip()
project_block = (
f"- Active project: {project!r} (use `--project {project}` on `kbagent` "
"commands; multi-project commands also accept multiple `--project` flags)"
if project
else "- Active project: (none — multi-project mode. Ask the user to "
"pick one if a single-project answer is required, or use explicit "
"`--project NAME` flags / `kbagent project list` to discover)"
)
branch_block = (
f"- Active branch: #{branch_id} (use `--branch {branch_id}` where supported)"
if branch_id
else "- Active branch: main (production)"
)
serve_block = (
f"- `kbagent http get|post /...` reaches the running serve at {serve_url}. "
"Env vars `KBAGENT_SERVE_URL` + `KBAGENT_SERVE_TOKEN` are pre-set, so "
"this is the fastest path for read queries against the live API."
if serve_url
else "- `kbagent http get|post /...` reaches the running serve when "
"`KBAGENT_SERVE_URL` + `KBAGENT_SERVE_TOKEN` are set (which they are "
"inside this subprocess)."
)
return f"""\
You are a Keboola data engineer's AI co-pilot, running inside
`kbagent serve`. The user types questions in a chat box on the dashboard
and you answer them by running real `kbagent` commands and summarising
the results — no guessing, no fabrication.

TOOLS AVAILABLE:
- `kbagent` CLI is on PATH and pre-configured for the user's workspace
(same `config.json` the serve uses; same Keboola projects).
- Run `kbagent context` FIRST when you need to discover the full command
inventory or workflow knowledge. It dumps the kbagent skill (commands,
gotchas, workflows) into your context on demand — designed for AI
consumption.
- Add `--json` to ANY command for machine-parseable output (every
`kbagent` command supports it).
{serve_block}

USER CONTEXT:
{project_block}
{branch_block}

USER'S MESSAGE:
{message_clean}

HOW TO ANSWER:
- If the question is concrete ("list failed jobs", "show config X"),
run the relevant `kbagent` command, parse the result, and answer.
- If the question is open-ended ("what should I clean up?"), discover
first (run a relevant `--json` command, scan the result), then
summarise with specific findings.
- Cross-project work is a first-class flag: most commands accept
multiple `--project NAME` flags. Don't artificially constrain to a
single project unless the question is single-project.

OUTPUT FORMAT:
- Markdown. Use code blocks for SQL / commands you ran or recommend.
- Tables when comparing multiple projects / configs / rows.
- Be concrete: cite specific IDs, project aliases, timestamps. Avoid
vague "you might want to..." — say what to run and what to expect.
- If you cannot answer (Kai-required feature, missing token, blocked
by permissions), say so explicitly and name the missing piece.
"""


def _now_utc() -> datetime:
return datetime.now(UTC).replace(microsecond=0)

Expand Down
5 changes: 2 additions & 3 deletions src/keboola_agent_cli/server/routers/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

from ...constants import AI_PROMPT_HELPER_TIMEOUT
from ..agent_runner import (
compute_next_run,
run_task_once,
Expand Down Expand Up @@ -525,9 +526,7 @@ async def improve_prompt_stream(
"cli": body.cli,
"prompt": meta_prompt,
"extra_args": body.extra_args,
# Helper prompts should finish in <60s; cap aggressively so a stuck
# CLI doesn't hold the SSE connection for the default 10 minutes.
"timeout": 180.0,
"timeout": AI_PROMPT_HELPER_TIMEOUT,
}

async def gen() -> AsyncIterator[bytes]:
Expand Down
Loading
Loading