kagenti-cli 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.3
2
+ Name: kagenti-cli
3
+ Version: 0.7.1
4
+ Summary: Kagenti ADK CLI
5
+ Author: IBM Corp.
6
+ Requires-Dist: anyio>=4.12.1
7
+ Requires-Dist: pydantic>=2.12.5
8
+ Requires-Dist: pydantic-settings>=2.12.0
9
+ Requires-Dist: requests>=2.32.5
10
+ Requires-Dist: jsonschema>=4.26.0
11
+ Requires-Dist: jsf>=0.11.2
12
+ Requires-Dist: gnureadline>=8.3.3 ; sys_platform != 'win32'
13
+ Requires-Dist: prompt-toolkit>=3.0.52
14
+ Requires-Dist: inquirerpy>=0.3.4
15
+ Requires-Dist: psutil>=7.2.2
16
+ Requires-Dist: a2a-sdk
17
+ Requires-Dist: tenacity>=9.1.2
18
+ Requires-Dist: typer>=0.21.1
19
+ Requires-Dist: pyyaml>=6.0.3
20
+ Requires-Dist: kagenti-adk
21
+ Requires-Dist: authlib>=1.6.6
22
+ Requires-Dist: openai>=2.16.0
23
+ Requires-Python: >=3.14, <3.15
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Agent Stack CLI
@@ -0,0 +1 @@
1
+ # Agent Stack CLI
@@ -0,0 +1,72 @@
1
+ [project]
2
+ name = "kagenti-cli"
3
+ version = "0.7.1"
4
+ description = "Kagenti ADK CLI"
5
+ readme = "README.md"
6
+ authors = [{ name = "IBM Corp." }]
7
+ requires-python = ">=3.14,<3.15"
8
+ dependencies = [
9
+ "anyio>=4.12.1",
10
+ "pydantic>=2.12.5",
11
+ "pydantic-settings>=2.12.0",
12
+ "requests>=2.32.5",
13
+ "jsonschema>=4.26.0",
14
+ "jsf>=0.11.2",
15
+ 'gnureadline>=8.3.3; sys_platform != "win32"',
16
+ "prompt-toolkit>=3.0.52",
17
+ "inquirerpy>=0.3.4",
18
+ "psutil>=7.2.2",
19
+ "a2a-sdk", # version determined by kagenti-adk
20
+ "tenacity>=9.1.2",
21
+ "typer>=0.21.1",
22
+ "pyyaml>=6.0.3",
23
+ "kagenti-adk",
24
+ "authlib>=1.6.6",
25
+ "openai>=2.16.0",
26
+ ]
27
+
28
+ [dependency-groups]
29
+ dev = ["pyrefly>=0.52.0", "pytest>=9.0.2", "ruff>=0.14.14", "wheel>=0.46.3"]
30
+
31
+ [tool.uv.sources]
32
+ kagenti-adk = { path = "../adk-py", editable = true }
33
+
34
+ [project.scripts]
35
+ kagenti-adk = "kagenti_cli:app"
36
+
37
+ [build-system]
38
+ requires = ["uv_build>=0.10.0,<0.11.0"]
39
+ build-backend = "uv_build"
40
+
41
+ [tool.uv.build-backend]
42
+ source-include = ["src/kagenti_cli/data"]
43
+ source-exclude = ["**/.DS_Store"]
44
+
45
+ [tool.ruff]
46
+ line-length = 120
47
+ target-version = "py314"
48
+ lint.select = [
49
+ "E", # pycodestyle errors
50
+ "W", # pycodestyle warnings
51
+ "F", # pyflakes
52
+ "UP", # pyupgrade
53
+ "I", # isort
54
+ "B", # bugbear
55
+ "N", # pep8-naming
56
+ "C4", # Comprehensions
57
+ "Q", # Quotes
58
+ "SIM", # Simplify
59
+ "RUF", # Ruff
60
+ "TID", # tidy-imports
61
+ "ASYNC", # async
62
+ # TODO: add "DTZ", # DatetimeZ
63
+ # TODO: add "ANN", # annotations
64
+ ]
65
+ lint.ignore = [
66
+ "E501", # line lenght (annyoing)
67
+ "UP013", # update TypedDict to longer syntax -- not necessary
68
+ ]
69
+ force-exclude = true
70
+
71
+ [tool.pyrefly]
72
+ project-includes = ["**/*.py*", "**/*.ipynb"]
@@ -0,0 +1,172 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import logging
8
+ import re
9
+ import typing
10
+ from copy import deepcopy
11
+
12
+ import kagenti_adk # noqa: F401 -- imported early due to Pydantic patches
13
+ import typer
14
+
15
+ import kagenti_cli.commands.agent
16
+ import kagenti_cli.commands.build
17
+ import kagenti_cli.commands.connector
18
+ import kagenti_cli.commands.model
19
+ import kagenti_cli.commands.platform
20
+ import kagenti_cli.commands.self
21
+ import kagenti_cli.commands.server
22
+
23
+ # import kagenti_cli.commands.user
24
+ from kagenti_cli.async_typer import AsyncTyper
25
+ from kagenti_cli.configuration import Configuration
26
+
27
+ logging.basicConfig(level=logging.INFO if Configuration().debug else logging.FATAL)
28
+ logging.getLogger("httpx").setLevel(logging.WARNING) # not sure why this is necessary
29
+
30
+
31
+ HELP_TEXT = """\
32
+ Usage: kagenti-adk [OPTIONS] COMMAND [ARGS]...
33
+
34
+ ╭─ Getting Started ──────────────────────────────────────────────────────────╮
35
+ │ ui Launch the web interface │
36
+ │ list View all available agents │
37
+ │ info Show agent details │
38
+ │ run Run an agent interactively │
39
+ ╰────────────────────────────────────────────────────────────────────────────╯
40
+
41
+ ╭─ Agent Management [Admin only] ────────────────────────────────────────────╮
42
+ │ add Install an agent │
43
+ │ remove Uninstall an agent │
44
+ │ update Update an agent │
45
+ │ build Build an agent image locally │
46
+ ╰────────────────────────────────────────────────────────────────────────────╯
47
+
48
+ ╭─ Platform & Configuration ─────────────────────────────────────────────────╮
49
+ | connector Manage connectors to external services │
50
+ │ model Configure 15+ LLM providers [Admin only] │
51
+ │ platform Start, stop, or delete local platform [Local only] │
52
+ │ server Connect to remote Kagenti ADK servers │
53
+ │ self version Show Kagenti ADK CLI and Platform version │
54
+ │ self upgrade Upgrade Kagenti ADK CLI and Platform [Local only] │
55
+ │ self uninstall Uninstall Kagenti ADK CLI and Platform [Local only] │
56
+ ╰────────────────────────────────────────────────────────────────────────────╯
57
+
58
+ ╭─ Options ──────────────────────────────────────────────────────────────────╮
59
+ │ --version Show CLI version and exit │
60
+ │ --help Show this help message │
61
+ │ --show-completion Show tab completion script │
62
+ │ --install-completion Enable tab completion for commands │
63
+ ╰────────────────────────────────────────────────────────────────────────────╯
64
+ """
65
+
66
+
67
+ app = AsyncTyper()
68
+
69
+
70
+ @app.callback(invoke_without_command=True)
71
+ def main(
72
+ ctx: typer.Context,
73
+ help: bool = typer.Option(False, "--help", help="Show this message and exit."),
74
+ version: bool = typer.Option(False, "--version", help="Show CLI version and exit."),
75
+ ):
76
+ if version:
77
+ asyncio.run(kagenti_cli.commands.self.version())
78
+ raise typer.Exit()
79
+ if help or ctx.invoked_subcommand is None:
80
+ typer.echo(HELP_TEXT)
81
+ raise typer.Exit()
82
+
83
+
84
+ app.add_typer(
85
+ kagenti_cli.commands.model.app, name="model", no_args_is_help=True, help="Manage model providers. [Admin only]"
86
+ )
87
+ app.add_typer(
88
+ kagenti_cli.commands.agent.app,
89
+ name="agent",
90
+ no_args_is_help=True,
91
+ help="Manage agents. Some commands are [Admin only].",
92
+ )
93
+ app.add_typer(
94
+ kagenti_cli.commands.connector.app,
95
+ name="connector",
96
+ no_args_is_help=True,
97
+ help="Manage connectors to external services.",
98
+ )
99
+ app.add_typer(
100
+ kagenti_cli.commands.platform.app,
101
+ name="platform",
102
+ no_args_is_help=True,
103
+ help="Manage Kagenti ADK platform. [Local only]",
104
+ )
105
+ app.add_typer(
106
+ kagenti_cli.commands.server.app,
107
+ name="server",
108
+ no_args_is_help=True,
109
+ help="Manage Kagenti ADK servers and authentication.",
110
+ )
111
+ app.add_typer(
112
+ kagenti_cli.commands.self.app,
113
+ name="self",
114
+ no_args_is_help=True,
115
+ help="Manage Kagenti ADK installation.",
116
+ hidden=True,
117
+ )
118
+ # TODO: Implement keycloak integration
119
+ # app.add_typer(
120
+ # kagenti_cli.commands.user.app,
121
+ # name="user",
122
+ # no_args_is_help=True,
123
+ # help="Manage users. [Admin only]",
124
+ # )
125
+
126
+
127
+ app.add_typer(kagenti_cli.commands.build.app, name="", no_args_is_help=True, help="Build agent images.")
128
+
129
+ agent_alias = deepcopy(kagenti_cli.commands.agent.app)
130
+ for cmd in agent_alias.registered_commands:
131
+ cmd.rich_help_panel = "Agent commands"
132
+
133
+ app.add_typer(agent_alias, name="", no_args_is_help=True)
134
+
135
+
136
+ @app.command("version")
137
+ async def version(
138
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
139
+ ):
140
+ """Print version of the Kagenti ADK CLI."""
141
+ import kagenti_cli.commands.self
142
+
143
+ await kagenti_cli.commands.self.version(verbose=verbose)
144
+
145
+
146
+ @app.command("ui")
147
+ async def ui():
148
+ """Launch the graphical interface."""
149
+ import webbrowser
150
+
151
+ import kagenti_cli.commands.model
152
+
153
+ await kagenti_cli.commands.model.ensure_llm_provider()
154
+
155
+ config = Configuration()
156
+ active_server = config.auth_manager.active_server
157
+
158
+ if active_server:
159
+ if "agentstack-api.localtest.me" in active_server:
160
+ ui_url = active_server.replace("agentstack-api.localtest.me", "agentstack.localtest.me")
161
+ elif re.search(r"(localhost|127\.0\.0\.1):8333", active_server):
162
+ ui_url = re.sub(r":8333", ":8334", active_server)
163
+ else:
164
+ ui_url = active_server
165
+ else:
166
+ ui_url = "http://agentstack.localtest.me:8080"
167
+
168
+ webbrowser.open(ui_url)
169
+
170
+
171
+ if __name__ == "__main__":
172
+ app()
@@ -0,0 +1,6 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from kagenti_cli import app
5
+
6
+ app()
@@ -0,0 +1,167 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import re
8
+ import urllib
9
+ import urllib.parse
10
+ from collections.abc import AsyncIterator
11
+ from contextlib import asynccontextmanager
12
+ from datetime import timedelta
13
+ from textwrap import indent
14
+ from typing import Any
15
+
16
+ import httpx
17
+ import openai
18
+ import pydantic
19
+ from a2a.client import A2AClientError, Client, ClientConfig, ClientFactory
20
+ from a2a.types import AgentCard
21
+ from kagenti_adk.platform.context import ContextToken
22
+ from google.protobuf.json_format import MessageToDict
23
+ from httpx import HTTPStatusError
24
+ from httpx._types import RequestFiles
25
+
26
+ from kagenti_cli import configuration
27
+ from kagenti_cli.configuration import Configuration
28
+ from kagenti_cli.utils import pick
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ config = Configuration()
33
+
34
+ API_BASE_URL = "api/v1/"
35
+
36
+
37
+ async def api_request(
38
+ method: str,
39
+ path: str,
40
+ json: dict | None = None,
41
+ files: RequestFiles | None = None,
42
+ params: dict[str, Any] | None = None,
43
+ use_auth: bool = True,
44
+ ) -> dict | None:
45
+ """Make an API request to the server."""
46
+ async with configuration.use_platform_client() as client:
47
+ response = await client.request(
48
+ method,
49
+ urllib.parse.urljoin(API_BASE_URL, path),
50
+ json=json,
51
+ files=files,
52
+ params=params,
53
+ timeout=60,
54
+ headers=(
55
+ {"Authorization": f"Bearer {token.access_token}"}
56
+ if use_auth and (token := await config.auth_manager.load_auth_token())
57
+ else {}
58
+ ),
59
+ )
60
+ if response.is_error:
61
+ error = ""
62
+ try:
63
+ error = response.json()
64
+ error = error.get("detail", str(error))
65
+ except Exception:
66
+ response.raise_for_status()
67
+ if response.status_code == 401:
68
+ message = f'{error}\nexport KAGENTI_ADK_ADMIN_PASSWORD="<PASSWORD>" to set the admin password.'
69
+ raise HTTPStatusError(message=message, request=response.request, response=response)
70
+ raise HTTPStatusError(message=error, request=response.request, response=response)
71
+ if response.content:
72
+ return response.json()
73
+
74
+
75
+ async def api_stream(
76
+ method: str,
77
+ path: str,
78
+ json: dict | None = None,
79
+ params: dict[str, Any] | None = None,
80
+ use_auth: bool = True,
81
+ ) -> AsyncIterator[dict[str, Any]]:
82
+ """Make a streaming API request to the server."""
83
+ import json as jsonlib
84
+
85
+ async with (
86
+ configuration.use_platform_client() as client,
87
+ client.stream(
88
+ method,
89
+ urllib.parse.urljoin(API_BASE_URL, path),
90
+ json=json,
91
+ params=params,
92
+ timeout=timedelta(hours=1).total_seconds(),
93
+ headers=(
94
+ {"Authorization": f"Bearer {token.access_token}"}
95
+ if use_auth and (token := await config.auth_manager.load_auth_token())
96
+ else {}
97
+ ),
98
+ ) as response,
99
+ ):
100
+ response: httpx.Response
101
+ if response.is_error:
102
+ error = ""
103
+ try:
104
+ [error] = [jsonlib.loads(message) async for message in response.aiter_text()]
105
+ error = error.get("detail", str(error))
106
+ except Exception:
107
+ response.raise_for_status()
108
+ raise HTTPStatusError(message=error, request=response.request, response=response)
109
+ async for line in response.aiter_lines():
110
+ if line:
111
+ yield jsonlib.loads(re.sub("^data:", "", line).strip())
112
+
113
+
114
+ async def fetch_server_version() -> str | None:
115
+ """Fetch server version from OpenAPI schema."""
116
+
117
+ class OpenAPIInfo(pydantic.BaseModel):
118
+ version: str
119
+
120
+ class OpenAPISchema(pydantic.BaseModel):
121
+ info: OpenAPIInfo
122
+
123
+ try:
124
+ response = await api_request("GET", "openapi.json", use_auth=False)
125
+ if not response:
126
+ return None
127
+ schema = OpenAPISchema.model_validate(response)
128
+ return schema.info.version
129
+ except Exception as e:
130
+ logger.warning("Failed to fetch server version: %s", e)
131
+ return None
132
+
133
+
134
+ @asynccontextmanager
135
+ async def a2a_client(agent_card: AgentCard, context_token: ContextToken) -> AsyncIterator[Client]:
136
+ try:
137
+ async with httpx.AsyncClient(
138
+ headers={"Authorization": f"Bearer {context_token.token.get_secret_value()}"},
139
+ follow_redirects=True,
140
+ timeout=timedelta(hours=1).total_seconds(),
141
+ ) as httpx_client:
142
+ yield ClientFactory(ClientConfig(httpx_client=httpx_client, use_client_preference=True)).create(
143
+ card=agent_card
144
+ )
145
+ except A2AClientError as ex:
146
+ card_data = json.dumps(
147
+ pick(MessageToDict(agent_card), {"url", "additional_interfaces", "preferred_transport"}),
148
+ indent=2,
149
+ )
150
+ raise RuntimeError(
151
+ f"The agent is not reachable, please check that the agent card is configured properly.\n"
152
+ f"Agent connection info:\n{indent(card_data, prefix=' ')}\n"
153
+ "Full Error:\n"
154
+ f"{indent(str(ex), prefix=' ')}"
155
+ ) from ex
156
+
157
+
158
+ @asynccontextmanager
159
+ async def openai_client() -> AsyncIterator[openai.AsyncOpenAI]:
160
+ async with Configuration().use_platform_client() as platform_client:
161
+ headers = platform_client.headers.copy()
162
+ headers.pop("Authorization", None)
163
+ yield openai.AsyncOpenAI(
164
+ api_key=platform_client.headers.get("Authorization", "").removeprefix("Bearer ") or "dummy",
165
+ base_url=urllib.parse.urljoin(str(platform_client.base_url), urllib.parse.urljoin(API_BASE_URL, "openai")),
166
+ default_headers=headers,
167
+ )
@@ -0,0 +1,124 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import functools
8
+ import inspect
9
+ import re
10
+ import sys
11
+ from collections.abc import Iterator
12
+ from contextlib import contextmanager
13
+
14
+ import rich.text
15
+ import typer
16
+ from rich.console import RenderResult
17
+ from rich.markdown import Heading, Markdown
18
+ from rich.table import Table
19
+ from typer.core import TyperGroup
20
+
21
+ from kagenti_cli.configuration import Configuration
22
+ from kagenti_cli.console import console, err_console
23
+ from kagenti_cli.utils import extract_messages, format_error
24
+
25
+ DEBUG = Configuration().debug
26
+
27
+ sys.unraisablehook = lambda _: None # Suppress benign cleanup errors
28
+
29
+
30
+ class _LeftAlignedHeading(Heading):
31
+ def __rich_console__(self, *args, **kwargs) -> RenderResult:
32
+ for elem in super().__rich_console__(*args, **kwargs):
33
+ if isinstance(elem, rich.text.Text):
34
+ elem.justify = "left"
35
+ yield elem
36
+
37
+
38
+ Markdown.elements["heading_open"] = _LeftAlignedHeading
39
+
40
+
41
+ @contextmanager
42
+ def create_table(*args, no_wrap: bool = True, **kwargs) -> Iterator[Table]:
43
+ table = Table(*args, **kwargs, box=None, pad_edge=False, width=console.width, show_header=True)
44
+ yield table
45
+ for column in table.columns:
46
+ column.no_wrap = no_wrap
47
+ column.overflow = "ellipsis"
48
+ assert isinstance(column.header, str)
49
+ column.header = column.header.upper()
50
+
51
+ if not table.rows:
52
+ table._render = lambda *args, **kwargs: [rich.text.Text("<No items found>", style="italic")]
53
+
54
+
55
+ class AliasGroup(TyperGroup):
56
+ """Taken from https://github.com/fastapi/typer/issues/132#issuecomment-2417492805"""
57
+
58
+ _CMD_SPLIT_P = re.compile(r" ?[,|] ?")
59
+
60
+ def get_command(self, ctx, cmd_name):
61
+ cmd_name = self._group_cmd_name(cmd_name)
62
+ return super().get_command(ctx, cmd_name)
63
+
64
+ def _group_cmd_name(self, default_name):
65
+ for cmd in self.commands.values():
66
+ name = cmd.name
67
+ if name and default_name in self._CMD_SPLIT_P.split(name):
68
+ return name
69
+ return default_name
70
+
71
+
72
+ class AsyncTyper(typer.Typer):
73
+ def __init__(self, *args, **kwargs):
74
+ kwargs["cls"] = kwargs.get("cls", AliasGroup)
75
+ super().__init__(*args, **kwargs)
76
+
77
+ def command(self, *args, **kwargs):
78
+ parent_decorator = super().command(*args, **kwargs)
79
+
80
+ def decorator(f):
81
+ @functools.wraps(f)
82
+ def wrapped_f(*args, **kwargs):
83
+ if sys.stdout.isatty():
84
+ sys.stdout.write("\x1b[>0u") # disable Kitty Keyboard Protocol (VSCode bug workaround)
85
+ sys.stdout.flush()
86
+ try:
87
+ if inspect.iscoroutinefunction(f):
88
+ return asyncio.run(f(*args, **kwargs))
89
+ else:
90
+ return f(*args, **kwargs)
91
+ except* Exception as ex:
92
+ is_permission_error = False
93
+ is_connect_error = False
94
+ for exc_type, message in extract_messages(ex):
95
+ err_console.print(format_error(exc_type, message))
96
+ is_connect_error = is_connect_error or exc_type in ["ConnectionError", "ConnectError"]
97
+ is_permission_error = is_permission_error or (
98
+ exc_type == "HTTPStatusError" and "403" in message
99
+ )
100
+ err_console.print()
101
+ if is_connect_error:
102
+ err_console.hint(
103
+ "Start the Kagenti ADK platform using: [green]kagenti-adk platform start[/green]. If that does not help, run [green]kagenti-adk platform delete[/green] to clean up, then [green]kagenti-adk platform start[/green] again."
104
+ )
105
+ elif is_permission_error:
106
+ err_console.hint(
107
+ "This command requires higher permissions than your account currently has. Contact your administrator for assistance."
108
+ )
109
+ else:
110
+ err_console.hint(
111
+ "Are you having consistent problems? If so, try these troubleshooting steps: [green]kagenti-adk platform delete[/green] to remove the platform, and [green]kagenti-adk platform start[/green] to recreate it."
112
+ )
113
+ if DEBUG:
114
+ raise
115
+ sys.exit(1)
116
+ finally:
117
+ if sys.stdout.isatty():
118
+ sys.stdout.write("\x1b[<u")
119
+ sys.stdout.flush()
120
+
121
+ parent_decorator(wrapped_f)
122
+ return f
123
+
124
+ return decorator