padwan-cli 0.5.0__tar.gz → 0.6.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.
- padwan_cli-0.6.1/LICENSE +21 -0
- padwan_cli-0.6.1/PKG-INFO +91 -0
- padwan_cli-0.6.1/README.md +62 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/__init__.py +0 -2
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/batch/__init__.py +0 -2
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/batch/format.py +1 -3
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/batch/run.py +2 -4
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/batch/utils.py +1 -3
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/chat.py +68 -29
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/run.py +41 -8
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/talk.py +13 -5
- padwan_cli-0.6.1/padwan_cli/trace.py +68 -0
- padwan_cli-0.6.1/padwan_cli/utils.py +21 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/widgets.py +7 -6
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/pyproject.toml +28 -5
- padwan_cli-0.6.1/pyproject.toml.orig +81 -0
- padwan_cli-0.5.0/PKG-INFO +0 -49
- padwan_cli-0.5.0/README.md +0 -36
- padwan_cli-0.5.0/padwan_cli/utils.py +0 -7
- padwan_cli-0.5.0/pyproject.toml.orig +0 -55
- {padwan_cli-0.5.0 → padwan_cli-0.6.1}/padwan_cli/__main__.py +0 -0
padwan_cli-0.6.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 polarsen-io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: padwan-cli
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: CLI and TUI for padwan-ai
|
|
5
|
+
Author-email: julien.brayere@polarsen.io
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
13
|
+
Requires-Dist: padwan-ai[realtime]>=0.11.1
|
|
14
|
+
Requires-Dist: piou[tui]>=0.36.0
|
|
15
|
+
Requires-Dist: tracktolib>=0.74.0
|
|
16
|
+
Requires-Dist: padwan-ai[langfuse]>=0.11.1 ; extra == 'langfuse'
|
|
17
|
+
Requires-Dist: padwan-ai[otel]>=0.11.1 ; extra == 'otel'
|
|
18
|
+
Requires-Dist: opentelemetry-sdk>=1.44 ; extra == 'otel'
|
|
19
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.44 ; extra == 'otel'
|
|
20
|
+
Requires-Dist: sounddevice>=0.5 ; extra == 'voice'
|
|
21
|
+
Requires-Python: >=3.15
|
|
22
|
+
Project-URL: Homepage, https://polarsen-io.github.io/padwan-cli
|
|
23
|
+
Project-URL: Source, https://github.com/polarsen-io/padwan-cli
|
|
24
|
+
Project-URL: Changelog, https://github.com/polarsen-io/padwan-cli/blob/master/CHANGELOG.md
|
|
25
|
+
Provides-Extra: langfuse
|
|
26
|
+
Provides-Extra: otel
|
|
27
|
+
Provides-Extra: voice
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Padwan CLI
|
|
31
|
+
|
|
32
|
+
Interactive CLI/TUI for [`padwan-ai`](https://github.com/polarsen-io/padwan-ai).
|
|
33
|
+
|
|
34
|
+
> [!NOTE]
|
|
35
|
+
> This is a playground for experimenting with `padwan-ai` features (streaming, agents, MCP, thinking tokens, batch jobs, realtime voice) — not a production-grade tool. Expect rough edges and breaking changes.
|
|
36
|
+
|
|
37
|
+
<img alt="Chat demo" src="https://github.com/polarsen-io/padwan-cli/raw/master/docs/static/chat.gif" width="800"/>
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export OPENAI_API_KEY=...
|
|
43
|
+
uvx padwan-cli
|
|
44
|
+
# then /help for commands
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install padwan-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Local Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv sync --group dev
|
|
58
|
+
uv run padwan-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## One-shot Mode
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uvx padwan-cli "Hello" -m gpt-4o-mini
|
|
65
|
+
|
|
66
|
+
# attach files (images/audio/text, routed by type)
|
|
67
|
+
uvx padwan-cli "What's in this recording?" -m gpt-4o-audio-preview -f clip.mp3
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Voice Mode
|
|
71
|
+
|
|
72
|
+
Real-time speech-to-speech chat (needs the PortAudio system lib, e.g. `libportaudio2`):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uvx --from "padwan-cli[voice]" padwan-talk
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Tracing
|
|
79
|
+
|
|
80
|
+
Pass `--trace <backend>` (on the one-shot, `chat send`, and `talk` commands) to instrument all LLM calls with [padwan-ai's OTel GenAI telemetry](https://github.com/polarsen-io/padwan-ai/blob/master/docs/observability.md):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Langfuse, using the standard LANGFUSE_* env vars
|
|
84
|
+
export LANGFUSE_PUBLIC_KEY=... LANGFUSE_SECRET_KEY=... LANGFUSE_BASE_URL=...
|
|
85
|
+
uvx --from "padwan-cli[langfuse]" padwan-cli "Hello" --trace langfuse
|
|
86
|
+
|
|
87
|
+
# any OTLP collector, using the standard OTEL_EXPORTER_OTLP_* env vars
|
|
88
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
|
|
89
|
+
uvx --from "padwan-cli[otel]" padwan-cli "Hello" --trace otlp
|
|
90
|
+
```
|
|
91
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Padwan CLI
|
|
2
|
+
|
|
3
|
+
Interactive CLI/TUI for [`padwan-ai`](https://github.com/polarsen-io/padwan-ai).
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> This is a playground for experimenting with `padwan-ai` features (streaming, agents, MCP, thinking tokens, batch jobs, realtime voice) — not a production-grade tool. Expect rough edges and breaking changes.
|
|
7
|
+
|
|
8
|
+
<img alt="Chat demo" src="https://github.com/polarsen-io/padwan-cli/raw/master/docs/static/chat.gif" width="800"/>
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
export OPENAI_API_KEY=...
|
|
14
|
+
uvx padwan-cli
|
|
15
|
+
# then /help for commands
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install padwan-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Local Development
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv sync --group dev
|
|
29
|
+
uv run padwan-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## One-shot Mode
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uvx padwan-cli "Hello" -m gpt-4o-mini
|
|
36
|
+
|
|
37
|
+
# attach files (images/audio/text, routed by type)
|
|
38
|
+
uvx padwan-cli "What's in this recording?" -m gpt-4o-audio-preview -f clip.mp3
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Voice Mode
|
|
42
|
+
|
|
43
|
+
Real-time speech-to-speech chat (needs the PortAudio system lib, e.g. `libportaudio2`):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uvx --from "padwan-cli[voice]" padwan-talk
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Tracing
|
|
50
|
+
|
|
51
|
+
Pass `--trace <backend>` (on the one-shot, `chat send`, and `talk` commands) to instrument all LLM calls with [padwan-ai's OTel GenAI telemetry](https://github.com/polarsen-io/padwan-ai/blob/master/docs/observability.md):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Langfuse, using the standard LANGFUSE_* env vars
|
|
55
|
+
export LANGFUSE_PUBLIC_KEY=... LANGFUSE_SECRET_KEY=... LANGFUSE_BASE_URL=...
|
|
56
|
+
uvx --from "padwan-cli[langfuse]" padwan-cli "Hello" --trace langfuse
|
|
57
|
+
|
|
58
|
+
# any OTLP collector, using the standard OTEL_EXPORTER_OTLP_* env vars
|
|
59
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
|
|
60
|
+
uvx --from "padwan-cli[otel]" padwan-cli "Hello" --trace otlp
|
|
61
|
+
```
|
|
62
|
+
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
from typing import TYPE_CHECKING
|
|
4
2
|
|
|
5
3
|
from rich.table import Table
|
|
@@ -7,7 +5,7 @@ from rich.table import Table
|
|
|
7
5
|
from ..utils import console
|
|
8
6
|
|
|
9
7
|
if TYPE_CHECKING:
|
|
10
|
-
from
|
|
8
|
+
from padwan_ai.gemini import BatchJob
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
def format_job(job: BatchJob) -> None:
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import asyncio
|
|
4
2
|
from pathlib import Path
|
|
5
3
|
|
|
6
4
|
from piou import CommandGroup, Option
|
|
7
5
|
from piou.tui import TuiContext, TuiOption
|
|
8
6
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
7
|
+
from padwan_ai import GEMINI_MODELS
|
|
8
|
+
from padwan_ai.gemini import BatchRequest, BatchResult, GeminiClient
|
|
11
9
|
from ..widgets import BatchProgressWidget, BatchResultWidget
|
|
12
10
|
from .format import console, format_job, format_job_table
|
|
13
11
|
from .utils import load_prompts_from_file, save_results_to_file
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import csv
|
|
4
2
|
import json
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
from typing import TYPE_CHECKING
|
|
7
5
|
|
|
8
|
-
from
|
|
6
|
+
from padwan_ai.gemini import BatchRequest, BatchResult
|
|
9
7
|
|
|
10
8
|
if TYPE_CHECKING:
|
|
11
9
|
from typing import Any
|
|
@@ -4,13 +4,13 @@ import mimetypes
|
|
|
4
4
|
import time
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, cast, get_args
|
|
8
8
|
|
|
9
9
|
from piou import Option, CommandGroup
|
|
10
10
|
from piou.tui import PromptStyle, TuiContext, TuiOption
|
|
11
11
|
from textual.css.query import NoMatches
|
|
12
12
|
|
|
13
|
-
from
|
|
13
|
+
from padwan_ai import (
|
|
14
14
|
AgentSession,
|
|
15
15
|
ContentPart,
|
|
16
16
|
ConversationSnapshot,
|
|
@@ -19,17 +19,22 @@ from padwan_llm import (
|
|
|
19
19
|
McpTool,
|
|
20
20
|
McpTransport,
|
|
21
21
|
ToolCallContext,
|
|
22
|
+
audio_part,
|
|
22
23
|
image_part,
|
|
24
|
+
supports_audio,
|
|
23
25
|
supports_vision,
|
|
24
26
|
text_file_part,
|
|
25
27
|
text_part,
|
|
26
28
|
)
|
|
27
|
-
from
|
|
28
|
-
from
|
|
29
|
+
from padwan_ai.content import AudioFormat
|
|
30
|
+
from padwan_ai.gemini import GeminiClient
|
|
31
|
+
from padwan_ai.gemini.models import ThinkingConfig
|
|
32
|
+
from .trace import TRACE_BACKENDS, TraceBackend, enable_tracing
|
|
29
33
|
from .utils import ALL_MODELS, console
|
|
30
34
|
from .widgets import (
|
|
31
35
|
Attachment,
|
|
32
36
|
AttachmentBadge,
|
|
37
|
+
AttachmentKind,
|
|
33
38
|
ErrorMessage,
|
|
34
39
|
StreamingMessage,
|
|
35
40
|
ThoughtMessage,
|
|
@@ -47,7 +52,7 @@ DATAGOUV_MCP_URL = "https://mcp.data.gouv.fr/mcp"
|
|
|
47
52
|
class _InMemoryStore:
|
|
48
53
|
"""ConversationStore keeping snapshots in memory across /chat:send calls.
|
|
49
54
|
|
|
50
|
-
Implements the `
|
|
55
|
+
Implements the `padwan_ai.ConversationStore` protocol so that
|
|
51
56
|
`AgentSession.load` / `AgentSession.save` transparently preserve chat
|
|
52
57
|
history across successive command invocations within the same TUI run.
|
|
53
58
|
"""
|
|
@@ -95,15 +100,37 @@ def _format_tokens(session: AgentSession, ctx: TuiContext | None = None) -> str:
|
|
|
95
100
|
return " ".join(parts)
|
|
96
101
|
|
|
97
102
|
|
|
103
|
+
_AUDIO_FORMATS: tuple[AudioFormat, ...] = get_args(AudioFormat)
|
|
104
|
+
_KIND_LABELS = {"image": "images", "audio": "audio", "text": "files"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _audio_fmt(path: Path) -> AudioFormat | None:
|
|
108
|
+
"""Derive the audio format from the file extension, if it is a known one."""
|
|
109
|
+
fmt = path.suffix.removeprefix(".").lower()
|
|
110
|
+
return cast(AudioFormat, fmt) if fmt in _AUDIO_FORMATS else None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _unsupported_warning(
|
|
114
|
+
model: str, attachments: list[Attachment], *, suffix: str
|
|
115
|
+
) -> str | None:
|
|
116
|
+
"""Amber warning line for attachments the model can't consume, or None."""
|
|
117
|
+
skipped = [a for a in attachments if not a.supported]
|
|
118
|
+
if not skipped:
|
|
119
|
+
return None
|
|
120
|
+
kinds = "/".join(dict.fromkeys(_KIND_LABELS[a.kind] for a in skipped))
|
|
121
|
+
return f"{model} can't read {kinds} — {len(skipped)} {suffix}"
|
|
122
|
+
|
|
123
|
+
|
|
98
124
|
def _build_user_content(
|
|
99
125
|
text: str, attachments: list[Attachment]
|
|
100
126
|
) -> str | list[ContentPart]:
|
|
101
127
|
"""Assemble the user turn from typed text and queued attachments.
|
|
102
128
|
|
|
103
129
|
Returns the plain string when nothing is attached. Otherwise builds content
|
|
104
|
-
parts: the text, each text file inlined, and each supported image
|
|
105
|
-
|
|
106
|
-
from the payload — they still appear in the
|
|
130
|
+
parts: the text, each text file inlined, and each supported image or audio
|
|
131
|
+
file as a data payload. Unsupported media (model can't see/hear it) and
|
|
132
|
+
unreadable files are dropped from the payload — they still appear in the
|
|
133
|
+
sent badge.
|
|
107
134
|
"""
|
|
108
135
|
if not attachments:
|
|
109
136
|
return text
|
|
@@ -111,9 +138,15 @@ def _build_user_content(
|
|
|
111
138
|
if text:
|
|
112
139
|
parts.append(text_part(text))
|
|
113
140
|
for a in attachments:
|
|
114
|
-
if a.
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
if not a.supported:
|
|
142
|
+
continue
|
|
143
|
+
if a.kind == "image":
|
|
144
|
+
parts.append(image_part(a.path))
|
|
145
|
+
elif a.kind == "audio":
|
|
146
|
+
try:
|
|
147
|
+
parts.append(audio_part(a.path))
|
|
148
|
+
except OSError, ValueError:
|
|
149
|
+
continue
|
|
117
150
|
else:
|
|
118
151
|
try:
|
|
119
152
|
parts.append(text_file_part(a.path))
|
|
@@ -158,6 +191,12 @@ async def chat_send_fn(
|
|
|
158
191
|
"--mcp",
|
|
159
192
|
help=f"Streamable-HTTP MCP server URL(s) to expose as tools (e.g. {DATAGOUV_MCP_URL})",
|
|
160
193
|
),
|
|
194
|
+
trace: TraceBackend | None = Option(
|
|
195
|
+
None,
|
|
196
|
+
"--trace",
|
|
197
|
+
help="Export LLM telemetry to this backend",
|
|
198
|
+
choices=TRACE_BACKENDS,
|
|
199
|
+
),
|
|
161
200
|
ctx: TuiContext = TuiOption(),
|
|
162
201
|
) -> None:
|
|
163
202
|
"""Start a conversation. Use Ctrl+C to exit."""
|
|
@@ -214,6 +253,9 @@ async def chat_send_fn(
|
|
|
214
253
|
if not ok:
|
|
215
254
|
return
|
|
216
255
|
|
|
256
|
+
if trace:
|
|
257
|
+
enable_tracing(trace)
|
|
258
|
+
|
|
217
259
|
try:
|
|
218
260
|
client = LLMClient(model=model, on_thought=_on_thought, base_url=base_url)
|
|
219
261
|
if isinstance(client, GeminiClient):
|
|
@@ -261,24 +303,26 @@ async def chat_send_fn(
|
|
|
261
303
|
size = p.stat().st_size
|
|
262
304
|
except OSError:
|
|
263
305
|
continue
|
|
264
|
-
|
|
306
|
+
mime = mimetypes.guess_type(p)[0] or ""
|
|
307
|
+
kind: AttachmentKind
|
|
308
|
+
if mime.startswith("image/"):
|
|
309
|
+
kind, supported = "image", supports_vision(model)
|
|
310
|
+
elif mime.startswith("audio/"):
|
|
311
|
+
kind, supported = "audio", supports_audio(model, _audio_fmt(p))
|
|
312
|
+
else:
|
|
313
|
+
kind, supported = "text", True
|
|
265
314
|
pending.append(
|
|
266
315
|
Attachment(
|
|
267
316
|
path=p,
|
|
268
317
|
name=p.name,
|
|
269
318
|
size=size,
|
|
270
|
-
|
|
271
|
-
supported=
|
|
319
|
+
kind=kind,
|
|
320
|
+
supported=supported,
|
|
272
321
|
)
|
|
273
322
|
)
|
|
274
|
-
|
|
275
|
-
warning = (
|
|
276
|
-
f"{model} can't read images — {len(blind)} will be skipped"
|
|
277
|
-
if blind
|
|
278
|
-
else None
|
|
279
|
-
)
|
|
323
|
+
warning = _unsupported_warning(model, pending, suffix="will be skipped")
|
|
280
324
|
ctx.set_attachments(render_attachments(pending, warning=warning))
|
|
281
|
-
_set_attach_warning(
|
|
325
|
+
_set_attach_warning(warning is not None)
|
|
282
326
|
ctx.set_hint(
|
|
283
327
|
f"{len(pending)} attached — type a message · Ctrl+U clears"
|
|
284
328
|
)
|
|
@@ -298,13 +342,8 @@ async def chat_send_fn(
|
|
|
298
342
|
sent = list(pending)
|
|
299
343
|
ctx.mount_widget(UserMessage(user_input))
|
|
300
344
|
if sent:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
]
|
|
304
|
-
badge_warning = (
|
|
305
|
-
f"{model} can't read images — {len(skipped)} skipped"
|
|
306
|
-
if skipped
|
|
307
|
-
else None
|
|
345
|
+
badge_warning = _unsupported_warning(
|
|
346
|
+
model, sent, suffix="skipped"
|
|
308
347
|
)
|
|
309
348
|
ctx.mount_widget(
|
|
310
349
|
AttachmentBadge(sent, warning=badge_warning)
|
|
@@ -330,7 +369,7 @@ async def chat_send_fn(
|
|
|
330
369
|
except Exception as e:
|
|
331
370
|
# Surface the error inline: the outer handler is
|
|
332
371
|
# only reached after session teardown, which can
|
|
333
|
-
# hang on the MCP listener (see padwan-
|
|
372
|
+
# hang on the MCP listener (see padwan-ai mcp.py).
|
|
334
373
|
ctx.mount_widget(ErrorMessage(str(e)))
|
|
335
374
|
needs_new_text_widget = True
|
|
336
375
|
ctx.set_silent_queue(False)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import json
|
|
4
2
|
import sys
|
|
3
|
+
from pathlib import Path
|
|
5
4
|
from typing import Any
|
|
6
5
|
|
|
7
6
|
from piou import Cli, Option
|
|
@@ -9,21 +8,25 @@ from piou.tui import get_tui_context
|
|
|
9
8
|
|
|
10
9
|
from rich.table import Table
|
|
11
10
|
|
|
12
|
-
from
|
|
11
|
+
from padwan_ai import (
|
|
12
|
+
ANTHROPIC_MODELS,
|
|
13
13
|
GEMINI_MODELS,
|
|
14
14
|
GROK_MODELS,
|
|
15
15
|
MISTRAL_MODELS,
|
|
16
16
|
OPENAI_MODELS,
|
|
17
|
+
ContentPart,
|
|
17
18
|
LLMClient,
|
|
19
|
+
content_parts,
|
|
18
20
|
)
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
21
|
+
from padwan_ai._base import OnThought
|
|
22
|
+
from padwan_ai.conversation import Message
|
|
23
|
+
from padwan_ai.errors import Provider
|
|
22
24
|
|
|
23
25
|
from .utils import console
|
|
24
26
|
from .batch import batch_group
|
|
25
27
|
from .chat import chat_group
|
|
26
28
|
from .talk import talk_command
|
|
29
|
+
from .trace import TRACE_BACKENDS, TraceBackend, enable_tracing
|
|
27
30
|
|
|
28
31
|
CUSTOM_CSS = """
|
|
29
32
|
Rule.chat-mode {
|
|
@@ -64,6 +67,8 @@ def list_models(
|
|
|
64
67
|
provider_models["Mistral"] = list(MISTRAL_MODELS)
|
|
65
68
|
if not provider or provider == "grok":
|
|
66
69
|
provider_models["Grok"] = list(GROK_MODELS)
|
|
70
|
+
if not provider or provider == "anthropic":
|
|
71
|
+
provider_models["Anthropic"] = list(ANTHROPIC_MODELS)
|
|
67
72
|
|
|
68
73
|
if not provider_models:
|
|
69
74
|
console.print(f"[red]Unknown provider: {provider}[/red]")
|
|
@@ -91,9 +96,14 @@ def info() -> None:
|
|
|
91
96
|
table.add_row("Gemini", str(len(GEMINI_MODELS)))
|
|
92
97
|
table.add_row("Mistral", str(len(MISTRAL_MODELS)))
|
|
93
98
|
table.add_row("Grok", str(len(GROK_MODELS)))
|
|
99
|
+
table.add_row("Anthropic", str(len(ANTHROPIC_MODELS)))
|
|
94
100
|
|
|
95
101
|
total = (
|
|
96
|
-
len(OPENAI_MODELS)
|
|
102
|
+
len(OPENAI_MODELS)
|
|
103
|
+
+ len(GEMINI_MODELS)
|
|
104
|
+
+ len(MISTRAL_MODELS)
|
|
105
|
+
+ len(GROK_MODELS)
|
|
106
|
+
+ len(ANTHROPIC_MODELS)
|
|
97
107
|
)
|
|
98
108
|
table.add_section()
|
|
99
109
|
table.add_row("[bold]Total[/bold]", f"[bold]{total}[/bold]")
|
|
@@ -119,6 +129,18 @@ async def oneshot(
|
|
|
119
129
|
"--stream-thinking",
|
|
120
130
|
help="Stream model reasoning/thinking tokens to stderr (requires --stream)",
|
|
121
131
|
),
|
|
132
|
+
files: list[Path] | None = Option(
|
|
133
|
+
None,
|
|
134
|
+
"-f",
|
|
135
|
+
"--file",
|
|
136
|
+
help="Attach file(s) to the prompt (images/audio/text, routed by type)",
|
|
137
|
+
),
|
|
138
|
+
trace: TraceBackend | None = Option(
|
|
139
|
+
None,
|
|
140
|
+
"--trace",
|
|
141
|
+
help="Export LLM telemetry to this backend",
|
|
142
|
+
choices=TRACE_BACKENDS,
|
|
143
|
+
),
|
|
122
144
|
) -> None:
|
|
123
145
|
"""Send a single prompt and print the response."""
|
|
124
146
|
parsed_extra: dict[str, Any] | None = None
|
|
@@ -138,8 +160,19 @@ async def oneshot(
|
|
|
138
160
|
|
|
139
161
|
on_thought: OnThought | None = _thought_streamer if stream_thinking else None
|
|
140
162
|
|
|
163
|
+
if trace:
|
|
164
|
+
enable_tracing(trace)
|
|
165
|
+
|
|
166
|
+
content: str | list[ContentPart] = prompt
|
|
167
|
+
if files:
|
|
168
|
+
try:
|
|
169
|
+
content = content_parts(prompt, *files)
|
|
170
|
+
except (OSError, ValueError) as e:
|
|
171
|
+
console.print(f"[red]--file: {e}[/red]")
|
|
172
|
+
raise SystemExit(1)
|
|
173
|
+
|
|
141
174
|
client = LLMClient(model=model, base_url=base_url, on_thought=on_thought)
|
|
142
|
-
messages: list[Message] = [Message(role="user", content=
|
|
175
|
+
messages: list[Message] = [Message(role="user", content=content)]
|
|
143
176
|
async with client:
|
|
144
177
|
if stream:
|
|
145
178
|
async for chunk in client.stream_chat(messages, extra_params=parsed_extra):
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import asyncio
|
|
4
2
|
import contextlib
|
|
5
3
|
import functools
|
|
@@ -15,10 +13,11 @@ from piou import Option
|
|
|
15
13
|
from piou.tui import get_tui_context
|
|
16
14
|
from tracktolib.utils import BytesBuffer
|
|
17
15
|
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
16
|
+
from padwan_ai import RealtimeClient, RealtimeConnection, RealtimeServerEvent
|
|
17
|
+
from padwan_ai.errors import LLMError
|
|
18
|
+
from padwan_ai.openai.realtime import NO_TURN_DETECTION, REALTIME_SAMPLE_RATE
|
|
21
19
|
|
|
20
|
+
from .trace import TRACE_BACKENDS, TraceBackend, enable_tracing
|
|
22
21
|
from .utils import console
|
|
23
22
|
|
|
24
23
|
SR = REALTIME_SAMPLE_RATE # 24 kHz mono PCM16, as gpt-realtime expects
|
|
@@ -317,6 +316,12 @@ async def talk_command(
|
|
|
317
316
|
check: bool = Option(
|
|
318
317
|
False, "--check", help="List audio devices and key status, then exit"
|
|
319
318
|
),
|
|
319
|
+
trace: TraceBackend | None = Option(
|
|
320
|
+
None,
|
|
321
|
+
"--trace",
|
|
322
|
+
help="Export LLM telemetry to this backend",
|
|
323
|
+
choices=TRACE_BACKENDS,
|
|
324
|
+
),
|
|
320
325
|
) -> None:
|
|
321
326
|
"""Talk with a real-time voice assistant (speech-to-speech)."""
|
|
322
327
|
# A live mic/speaker session needs the whole terminal; the Textual TUI owns it,
|
|
@@ -343,6 +348,9 @@ async def talk_command(
|
|
|
343
348
|
console.print(f"model={model} voice={voice}")
|
|
344
349
|
return
|
|
345
350
|
|
|
351
|
+
if trace:
|
|
352
|
+
enable_tracing(trace)
|
|
353
|
+
|
|
346
354
|
push_to_talk = not hands_free
|
|
347
355
|
if push_to_talk and not sys.stdin.isatty():
|
|
348
356
|
console.print(
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import os
|
|
3
|
+
from typing import Literal, get_args
|
|
4
|
+
|
|
5
|
+
from .utils import console
|
|
6
|
+
|
|
7
|
+
__all__ = ("TRACE_BACKENDS", "TraceBackend", "enable_tracing")
|
|
8
|
+
|
|
9
|
+
TraceBackend = Literal["langfuse", "otlp"]
|
|
10
|
+
# piou only derives choices from a bare Literal, not `Literal | None`.
|
|
11
|
+
TRACE_BACKENDS: list[TraceBackend] = list(get_args(TraceBackend))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _missing_extra(backend: TraceBackend) -> SystemExit:
|
|
15
|
+
extra = "langfuse" if backend == "langfuse" else "otel"
|
|
16
|
+
console.print(f"[red]{backend} tracing dependencies not installed.[/red]")
|
|
17
|
+
console.print(f"[dim]Install the {extra} extra: `uv sync --extra {extra}`.[/dim]")
|
|
18
|
+
return SystemExit(1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _enable_langfuse() -> None:
|
|
22
|
+
from padwan_ai.langfuse import instrument
|
|
23
|
+
|
|
24
|
+
integration = instrument()
|
|
25
|
+
atexit.register(integration.shutdown)
|
|
26
|
+
console.print("[dim]Tracing enabled (Langfuse)[/dim]")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _enable_otlp() -> None:
|
|
30
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
|
31
|
+
OTLPMetricExporter,
|
|
32
|
+
)
|
|
33
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
34
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
35
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
36
|
+
from opentelemetry.sdk.resources import Resource
|
|
37
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
38
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
39
|
+
|
|
40
|
+
from padwan_ai import otel
|
|
41
|
+
|
|
42
|
+
resource = Resource.create({"service.name": "padwan-cli"})
|
|
43
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
44
|
+
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
|
|
45
|
+
meter_provider = MeterProvider(
|
|
46
|
+
resource=resource,
|
|
47
|
+
metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())],
|
|
48
|
+
)
|
|
49
|
+
otel.instrument(tracer_provider=tracer_provider, meter_provider=meter_provider)
|
|
50
|
+
atexit.register(meter_provider.shutdown)
|
|
51
|
+
atexit.register(tracer_provider.shutdown)
|
|
52
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
|
|
53
|
+
console.print(f"[dim]Tracing enabled (OTLP → {endpoint})[/dim]")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def enable_tracing(backend: TraceBackend) -> None:
|
|
57
|
+
"""Instrument padwan-ai clients for this process, exporting to `backend`.
|
|
58
|
+
|
|
59
|
+
`langfuse` reads the standard `LANGFUSE_*` env vars; `otlp` uses the
|
|
60
|
+
standard `OTEL_EXPORTER_OTLP_*` env vars. Exporters are flushed at exit.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
if backend == "langfuse":
|
|
64
|
+
_enable_langfuse()
|
|
65
|
+
else:
|
|
66
|
+
_enable_otlp()
|
|
67
|
+
except ImportError:
|
|
68
|
+
raise _missing_extra(backend)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
|
|
3
|
+
from padwan_ai import (
|
|
4
|
+
ANTHROPIC_MODELS,
|
|
5
|
+
GEMINI_MODELS,
|
|
6
|
+
GROK_MODELS,
|
|
7
|
+
MISTRAL_MODELS,
|
|
8
|
+
OPENAI_MODELS,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
ALL_MODELS = sorted(
|
|
14
|
+
[
|
|
15
|
+
*OPENAI_MODELS,
|
|
16
|
+
*GEMINI_MODELS,
|
|
17
|
+
*MISTRAL_MODELS,
|
|
18
|
+
*GROK_MODELS,
|
|
19
|
+
*ANTHROPIC_MODELS,
|
|
20
|
+
]
|
|
21
|
+
)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import json
|
|
4
2
|
from dataclasses import dataclass
|
|
5
3
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
7
5
|
|
|
8
6
|
from rich import box
|
|
9
7
|
from rich.columns import Columns
|
|
@@ -15,7 +13,7 @@ from textual.widgets import Static
|
|
|
15
13
|
from piou.tui import StreamingMessage as _StreamingMessage
|
|
16
14
|
|
|
17
15
|
if TYPE_CHECKING:
|
|
18
|
-
from
|
|
16
|
+
from padwan_ai.gemini import BatchJob, BatchResult
|
|
19
17
|
|
|
20
18
|
# Colors for chat messages
|
|
21
19
|
USER_COLOR = "#87ceeb"
|
|
@@ -29,6 +27,9 @@ CHIP_BG = "#1f2230"
|
|
|
29
27
|
CHIP_BORDER = "#3a3f55"
|
|
30
28
|
|
|
31
29
|
|
|
30
|
+
AttachmentKind = Literal["image", "audio", "text"]
|
|
31
|
+
|
|
32
|
+
|
|
32
33
|
@dataclass
|
|
33
34
|
class Attachment:
|
|
34
35
|
"""A file dropped into the chat, queued to send with the next message."""
|
|
@@ -36,8 +37,8 @@ class Attachment:
|
|
|
36
37
|
path: Path
|
|
37
38
|
name: str
|
|
38
39
|
size: int
|
|
39
|
-
|
|
40
|
-
supported: bool # whether the active model can use it (images need
|
|
40
|
+
kind: AttachmentKind
|
|
41
|
+
supported: bool # whether the active model can use it (images/audio need support)
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def human_size(num_bytes: int) -> str:
|
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "padwan-cli"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "CLI and TUI for padwan-
|
|
3
|
+
version = "0.6.1"
|
|
4
|
+
description = "CLI and TUI for padwan-ai"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.15"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
14
|
+
"Programming Language :: Python :: 3.15",
|
|
15
|
+
]
|
|
7
16
|
dependencies = [
|
|
8
|
-
"padwan-
|
|
17
|
+
"padwan-ai[realtime]>=0.11.1",
|
|
9
18
|
"piou[tui]>=0.36.0",
|
|
10
19
|
"tracktolib>=0.74.0",
|
|
11
20
|
]
|
|
12
21
|
|
|
13
22
|
[[project.authors]]
|
|
14
|
-
email = "julien.brayere@polarsen.
|
|
23
|
+
email = "julien.brayere@polarsen.io"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://polarsen-io.github.io/padwan-cli"
|
|
27
|
+
Source = "https://github.com/polarsen-io/padwan-cli"
|
|
28
|
+
Changelog = "https://github.com/polarsen-io/padwan-cli/blob/master/CHANGELOG.md"
|
|
15
29
|
|
|
16
30
|
[project.optional-dependencies]
|
|
17
31
|
voice = ["sounddevice>=0.5"]
|
|
32
|
+
otel = [
|
|
33
|
+
"padwan-ai[otel]>=0.11.1",
|
|
34
|
+
"opentelemetry-sdk>=1.44",
|
|
35
|
+
"opentelemetry-exporter-otlp>=1.44",
|
|
36
|
+
]
|
|
37
|
+
langfuse = ["padwan-ai[langfuse]>=0.11.1"]
|
|
18
38
|
|
|
19
39
|
[project.scripts]
|
|
20
40
|
padwan-cli = "padwan_cli:main_tui"
|
|
@@ -45,6 +65,9 @@ dev = [
|
|
|
45
65
|
"pytest-asyncio>=1.3.0",
|
|
46
66
|
"pytest-cov>=7.0.0",
|
|
47
67
|
"ruff>=0.11.0",
|
|
68
|
+
"padwan-ai[langfuse]>=0.11.1",
|
|
69
|
+
"opentelemetry-sdk>=1.44",
|
|
70
|
+
"opentelemetry-exporter-otlp>=1.44",
|
|
48
71
|
]
|
|
49
72
|
docs = ["zensical>=0.0.31"]
|
|
50
73
|
bump = ["commitizen>=4.0.0"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "padwan-cli"
|
|
3
|
+
version = "0.6.1"
|
|
4
|
+
description = "CLI and TUI for padwan-ai"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ email = "julien.brayere@polarsen.io" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.15"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.15",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"padwan-ai[realtime]>=0.11.1",
|
|
21
|
+
"piou[tui]>=0.36.0",
|
|
22
|
+
"tracktolib>=0.74.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://polarsen-io.github.io/padwan-cli"
|
|
27
|
+
Source = "https://github.com/polarsen-io/padwan-cli"
|
|
28
|
+
Changelog = "https://github.com/polarsen-io/padwan-cli/blob/master/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
# `talk` command audio I/O; also needs the PortAudio system lib (libportaudio2).
|
|
32
|
+
voice = ["sounddevice>=0.5"]
|
|
33
|
+
# `--trace otlp`: OTel instrumentation with an OTLP exporter.
|
|
34
|
+
otel = [
|
|
35
|
+
"padwan-ai[otel]>=0.11.1",
|
|
36
|
+
"opentelemetry-sdk>=1.44",
|
|
37
|
+
"opentelemetry-exporter-otlp>=1.44",
|
|
38
|
+
]
|
|
39
|
+
# `--trace langfuse`: OTel instrumentation with the Langfuse adapter.
|
|
40
|
+
langfuse = ["padwan-ai[langfuse]>=0.11.1"]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
padwan-cli = "padwan_cli:main_tui"
|
|
44
|
+
padwan-talk = "padwan_cli.talk:main"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["uv_build>=0.9.28,<0.10.0"]
|
|
48
|
+
build-backend = "uv_build"
|
|
49
|
+
|
|
50
|
+
[tool.uv.build-backend]
|
|
51
|
+
module-root = ""
|
|
52
|
+
|
|
53
|
+
[dependency-groups]
|
|
54
|
+
dev = [
|
|
55
|
+
"piou[tui-reload]>=0.36.0",
|
|
56
|
+
"pyright>=1.1.408",
|
|
57
|
+
"pytest>=8.0.0",
|
|
58
|
+
"pytest-asyncio>=1.3.0",
|
|
59
|
+
"pytest-cov>=7.0.0",
|
|
60
|
+
"ruff>=0.11.0",
|
|
61
|
+
# keep the otel and langfuse extras type-checkable/testable in dev
|
|
62
|
+
"padwan-ai[langfuse]>=0.11.1",
|
|
63
|
+
"opentelemetry-sdk>=1.44",
|
|
64
|
+
"opentelemetry-exporter-otlp>=1.44",
|
|
65
|
+
]
|
|
66
|
+
docs = [
|
|
67
|
+
"zensical>=0.0.31",
|
|
68
|
+
]
|
|
69
|
+
bump = [
|
|
70
|
+
"commitizen>=4.0.0",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.commitizen]
|
|
74
|
+
name = "cz_conventional_commits"
|
|
75
|
+
version_provider = "pep621"
|
|
76
|
+
tag_format = "$version"
|
|
77
|
+
update_changelog_on_bump = true
|
|
78
|
+
bump_message = "chore(release): release $current_version → $new_version [skip ci]"
|
|
79
|
+
|
|
80
|
+
[tool.pytest.ini_options]
|
|
81
|
+
asyncio_mode = "auto"
|
padwan_cli-0.5.0/PKG-INFO
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: padwan-cli
|
|
3
|
-
Version: 0.5.0
|
|
4
|
-
Summary: CLI and TUI for padwan-llm
|
|
5
|
-
Author-email: julien.brayere@polarsen.com
|
|
6
|
-
Requires-Dist: padwan-llm[realtime]>=0.8.0
|
|
7
|
-
Requires-Dist: piou[tui]>=0.36.0
|
|
8
|
-
Requires-Dist: tracktolib>=0.74.0
|
|
9
|
-
Requires-Dist: sounddevice>=0.5 ; extra == 'voice'
|
|
10
|
-
Requires-Python: >=3.14
|
|
11
|
-
Provides-Extra: voice
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
|
-
# Padwan CLI
|
|
15
|
-
|
|
16
|
-
Interactive CLI/TUI for [`padwan-llm`](https://github.com/polarsen-io/padwan-llm).
|
|
17
|
-
|
|
18
|
-
> [!NOTE]
|
|
19
|
-
> This is a playground for experimenting with `padwan-llm` features (streaming, agents, MCP, thinking tokens, batch jobs) — not a production-grade tool. Expect rough edges and breaking changes.
|
|
20
|
-
|
|
21
|
-
<img alt="Chat demo" src="https://github.com/polarsen-io/padwan-cli/raw/master/docs/static/chat.gif" width="800"/>
|
|
22
|
-
|
|
23
|
-
## Quick start
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
export OPENAI_API_KEY=...
|
|
27
|
-
uvx padwan-cli
|
|
28
|
-
# then /help for commands
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
## Install
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
pip install padwan-cli
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Local Development
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
uv sync --group dev
|
|
42
|
-
uv run padwan-cli
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## One-shot Mode
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
uvx padwan-cli "Hello" -m gpt-4o-mini
|
|
49
|
-
```
|
padwan_cli-0.5.0/README.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Padwan CLI
|
|
2
|
-
|
|
3
|
-
Interactive CLI/TUI for [`padwan-llm`](https://github.com/polarsen-io/padwan-llm).
|
|
4
|
-
|
|
5
|
-
> [!NOTE]
|
|
6
|
-
> This is a playground for experimenting with `padwan-llm` features (streaming, agents, MCP, thinking tokens, batch jobs) — not a production-grade tool. Expect rough edges and breaking changes.
|
|
7
|
-
|
|
8
|
-
<img alt="Chat demo" src="https://github.com/polarsen-io/padwan-cli/raw/master/docs/static/chat.gif" width="800"/>
|
|
9
|
-
|
|
10
|
-
## Quick start
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
export OPENAI_API_KEY=...
|
|
14
|
-
uvx padwan-cli
|
|
15
|
-
# then /help for commands
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## Install
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
pip install padwan-cli
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Local Development
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
uv sync --group dev
|
|
29
|
-
uv run padwan-cli
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## One-shot Mode
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
uvx padwan-cli "Hello" -m gpt-4o-mini
|
|
36
|
-
```
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "padwan-cli"
|
|
3
|
-
version = "0.5.0"
|
|
4
|
-
description = "CLI and TUI for padwan-llm"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
authors = [
|
|
7
|
-
{ email = "julien.brayere@polarsen.com" }
|
|
8
|
-
]
|
|
9
|
-
requires-python = ">=3.14"
|
|
10
|
-
dependencies = [
|
|
11
|
-
"padwan-llm[realtime]>=0.8.0",
|
|
12
|
-
"piou[tui]>=0.36.0",
|
|
13
|
-
"tracktolib>=0.74.0",
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
[project.optional-dependencies]
|
|
17
|
-
# `talk` command audio I/O; also needs the PortAudio system lib (libportaudio2).
|
|
18
|
-
voice = ["sounddevice>=0.5"]
|
|
19
|
-
|
|
20
|
-
[project.scripts]
|
|
21
|
-
padwan-cli = "padwan_cli:main_tui"
|
|
22
|
-
padwan-talk = "padwan_cli.talk:main"
|
|
23
|
-
|
|
24
|
-
[build-system]
|
|
25
|
-
requires = ["uv_build>=0.9.28,<0.10.0"]
|
|
26
|
-
build-backend = "uv_build"
|
|
27
|
-
|
|
28
|
-
[tool.uv.build-backend]
|
|
29
|
-
module-root = ""
|
|
30
|
-
|
|
31
|
-
[dependency-groups]
|
|
32
|
-
dev = [
|
|
33
|
-
"piou[tui-reload]>=0.36.0",
|
|
34
|
-
"pyright>=1.1.408",
|
|
35
|
-
"pytest>=8.0.0",
|
|
36
|
-
"pytest-asyncio>=1.3.0",
|
|
37
|
-
"pytest-cov>=7.0.0",
|
|
38
|
-
"ruff>=0.11.0",
|
|
39
|
-
]
|
|
40
|
-
docs = [
|
|
41
|
-
"zensical>=0.0.31",
|
|
42
|
-
]
|
|
43
|
-
bump = [
|
|
44
|
-
"commitizen>=4.0.0",
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
[tool.commitizen]
|
|
48
|
-
name = "cz_conventional_commits"
|
|
49
|
-
version_provider = "pep621"
|
|
50
|
-
tag_format = "$version"
|
|
51
|
-
update_changelog_on_bump = true
|
|
52
|
-
bump_message = "chore(release): release $current_version → $new_version [skip ci]"
|
|
53
|
-
|
|
54
|
-
[tool.pytest.ini_options]
|
|
55
|
-
asyncio_mode = "auto"
|
|
File without changes
|