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.
- kagenti_cli-0.7.1/PKG-INFO +26 -0
- kagenti_cli-0.7.1/README.md +1 -0
- kagenti_cli-0.7.1/pyproject.toml +72 -0
- kagenti_cli-0.7.1/src/kagenti_cli/__init__.py +172 -0
- kagenti_cli-0.7.1/src/kagenti_cli/__main__.py +6 -0
- kagenti_cli-0.7.1/src/kagenti_cli/api.py +167 -0
- kagenti_cli-0.7.1/src/kagenti_cli/async_typer.py +124 -0
- kagenti_cli-0.7.1/src/kagenti_cli/auth_manager.py +407 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/__init__.py +5 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/agent.py +1484 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/build.py +103 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/connector.py +304 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/model.py +914 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/platform.py +980 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/self.py +216 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/server.py +396 -0
- kagenti_cli-0.7.1/src/kagenti_cli/commands/user.py +89 -0
- kagenti_cli-0.7.1/src/kagenti_cli/configuration.py +102 -0
- kagenti_cli-0.7.1/src/kagenti_cli/console.py +27 -0
- kagenti_cli-0.7.1/src/kagenti_cli/data/.gitignore +1 -0
- kagenti_cli-0.7.1/src/kagenti_cli/data/helm-chart.tgz +0 -0
- kagenti_cli-0.7.1/src/kagenti_cli/kagenti_client.py +77 -0
- kagenti_cli-0.7.1/src/kagenti_cli/server_utils.py +40 -0
- kagenti_cli-0.7.1/src/kagenti_cli/utils.py +480 -0
|
@@ -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,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
|