padwan-cli 0.5.0__tar.gz → 0.6.0__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.0/PKG-INFO +81 -0
- padwan_cli-0.6.0/README.md +62 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/chat.py +63 -24
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/run.py +37 -2
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/talk.py +10 -0
- padwan_cli-0.6.0/padwan_cli/trace.py +70 -0
- padwan_cli-0.6.0/padwan_cli/utils.py +21 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/widgets.py +6 -3
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/pyproject.toml +11 -2
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/pyproject.toml.orig +14 -2
- 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 → padwan_cli-0.6.0}/padwan_cli/__init__.py +0 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/__main__.py +0 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/batch/__init__.py +0 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/batch/format.py +0 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/batch/run.py +0 -0
- {padwan_cli-0.5.0 → padwan_cli-0.6.0}/padwan_cli/batch/utils.py +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: padwan-cli
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: CLI and TUI for padwan-llm
|
|
5
|
+
Author-email: julien.brayere@polarsen.com
|
|
6
|
+
Requires-Dist: padwan-llm[realtime]>=0.9.0
|
|
7
|
+
Requires-Dist: piou[tui]>=0.36.0
|
|
8
|
+
Requires-Dist: tracktolib>=0.74.0
|
|
9
|
+
Requires-Dist: padwan-llm[langfuse]>=0.9.0 ; extra == 'langfuse'
|
|
10
|
+
Requires-Dist: padwan-llm[otel]>=0.9.0 ; extra == 'otel'
|
|
11
|
+
Requires-Dist: opentelemetry-sdk>=1.44 ; extra == 'otel'
|
|
12
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.44 ; extra == 'otel'
|
|
13
|
+
Requires-Dist: sounddevice>=0.5 ; extra == 'voice'
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Provides-Extra: langfuse
|
|
16
|
+
Provides-Extra: otel
|
|
17
|
+
Provides-Extra: voice
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Padwan CLI
|
|
21
|
+
|
|
22
|
+
Interactive CLI/TUI for [`padwan-llm`](https://github.com/polarsen-io/padwan-llm).
|
|
23
|
+
|
|
24
|
+
> [!NOTE]
|
|
25
|
+
> This is a playground for experimenting with `padwan-llm` features (streaming, agents, MCP, thinking tokens, batch jobs, realtime voice) — not a production-grade tool. Expect rough edges and breaking changes.
|
|
26
|
+
|
|
27
|
+
<img alt="Chat demo" src="https://github.com/polarsen-io/padwan-cli/raw/master/docs/static/chat.gif" width="800"/>
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export OPENAI_API_KEY=...
|
|
33
|
+
uvx padwan-cli
|
|
34
|
+
# then /help for commands
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install padwan-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Local Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv sync --group dev
|
|
48
|
+
uv run padwan-cli
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## One-shot Mode
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uvx padwan-cli "Hello" -m gpt-4o-mini
|
|
55
|
+
|
|
56
|
+
# attach files (images/audio/text, routed by type)
|
|
57
|
+
uvx padwan-cli "What's in this recording?" -m gpt-4o-audio-preview -f clip.mp3
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Voice Mode
|
|
61
|
+
|
|
62
|
+
Real-time speech-to-speech chat (needs the PortAudio system lib, e.g. `libportaudio2`):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uvx --from "padwan-cli[voice]" padwan-talk
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Tracing
|
|
69
|
+
|
|
70
|
+
Pass `--trace <backend>` (on the one-shot, `chat send`, and `talk` commands) to instrument all LLM calls with [padwan-llm's OTel GenAI telemetry](https://github.com/polarsen-io/padwan-llm/blob/master/docs/observability.md):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Langfuse, using the standard LANGFUSE_* env vars
|
|
74
|
+
export LANGFUSE_PUBLIC_KEY=... LANGFUSE_SECRET_KEY=... LANGFUSE_BASE_URL=...
|
|
75
|
+
uvx --from "padwan-cli[langfuse]" padwan-cli "Hello" --trace langfuse
|
|
76
|
+
|
|
77
|
+
# any OTLP collector, using the standard OTEL_EXPORTER_OTLP_* env vars
|
|
78
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
|
|
79
|
+
uvx --from "padwan-cli[otel]" padwan-cli "Hello" --trace otlp
|
|
80
|
+
```
|
|
81
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
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, 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-llm's OTel GenAI telemetry](https://github.com/polarsen-io/padwan-llm/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
|
+
|
|
@@ -4,7 +4,7 @@ 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
|
|
@@ -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
|
)
|
|
29
|
+
from padwan_llm.content import AudioFormat
|
|
27
30
|
from padwan_llm.gemini import GeminiClient
|
|
28
31
|
from padwan_llm.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,
|
|
@@ -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)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import sys
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from piou import Cli, Option
|
|
@@ -10,11 +11,14 @@ from piou.tui import get_tui_context
|
|
|
10
11
|
from rich.table import Table
|
|
11
12
|
|
|
12
13
|
from padwan_llm import (
|
|
14
|
+
ANTHROPIC_MODELS,
|
|
13
15
|
GEMINI_MODELS,
|
|
14
16
|
GROK_MODELS,
|
|
15
17
|
MISTRAL_MODELS,
|
|
16
18
|
OPENAI_MODELS,
|
|
19
|
+
ContentPart,
|
|
17
20
|
LLMClient,
|
|
21
|
+
content_parts,
|
|
18
22
|
)
|
|
19
23
|
from padwan_llm._base import OnThought
|
|
20
24
|
from padwan_llm.conversation import Message
|
|
@@ -24,6 +28,7 @@ from .utils import console
|
|
|
24
28
|
from .batch import batch_group
|
|
25
29
|
from .chat import chat_group
|
|
26
30
|
from .talk import talk_command
|
|
31
|
+
from .trace import TRACE_BACKENDS, TraceBackend, enable_tracing
|
|
27
32
|
|
|
28
33
|
CUSTOM_CSS = """
|
|
29
34
|
Rule.chat-mode {
|
|
@@ -64,6 +69,8 @@ def list_models(
|
|
|
64
69
|
provider_models["Mistral"] = list(MISTRAL_MODELS)
|
|
65
70
|
if not provider or provider == "grok":
|
|
66
71
|
provider_models["Grok"] = list(GROK_MODELS)
|
|
72
|
+
if not provider or provider == "anthropic":
|
|
73
|
+
provider_models["Anthropic"] = list(ANTHROPIC_MODELS)
|
|
67
74
|
|
|
68
75
|
if not provider_models:
|
|
69
76
|
console.print(f"[red]Unknown provider: {provider}[/red]")
|
|
@@ -91,9 +98,14 @@ def info() -> None:
|
|
|
91
98
|
table.add_row("Gemini", str(len(GEMINI_MODELS)))
|
|
92
99
|
table.add_row("Mistral", str(len(MISTRAL_MODELS)))
|
|
93
100
|
table.add_row("Grok", str(len(GROK_MODELS)))
|
|
101
|
+
table.add_row("Anthropic", str(len(ANTHROPIC_MODELS)))
|
|
94
102
|
|
|
95
103
|
total = (
|
|
96
|
-
len(OPENAI_MODELS)
|
|
104
|
+
len(OPENAI_MODELS)
|
|
105
|
+
+ len(GEMINI_MODELS)
|
|
106
|
+
+ len(MISTRAL_MODELS)
|
|
107
|
+
+ len(GROK_MODELS)
|
|
108
|
+
+ len(ANTHROPIC_MODELS)
|
|
97
109
|
)
|
|
98
110
|
table.add_section()
|
|
99
111
|
table.add_row("[bold]Total[/bold]", f"[bold]{total}[/bold]")
|
|
@@ -119,6 +131,18 @@ async def oneshot(
|
|
|
119
131
|
"--stream-thinking",
|
|
120
132
|
help="Stream model reasoning/thinking tokens to stderr (requires --stream)",
|
|
121
133
|
),
|
|
134
|
+
files: list[Path] | None = Option(
|
|
135
|
+
None,
|
|
136
|
+
"-f",
|
|
137
|
+
"--file",
|
|
138
|
+
help="Attach file(s) to the prompt (images/audio/text, routed by type)",
|
|
139
|
+
),
|
|
140
|
+
trace: TraceBackend | None = Option(
|
|
141
|
+
None,
|
|
142
|
+
"--trace",
|
|
143
|
+
help="Export LLM telemetry to this backend",
|
|
144
|
+
choices=TRACE_BACKENDS,
|
|
145
|
+
),
|
|
122
146
|
) -> None:
|
|
123
147
|
"""Send a single prompt and print the response."""
|
|
124
148
|
parsed_extra: dict[str, Any] | None = None
|
|
@@ -138,8 +162,19 @@ async def oneshot(
|
|
|
138
162
|
|
|
139
163
|
on_thought: OnThought | None = _thought_streamer if stream_thinking else None
|
|
140
164
|
|
|
165
|
+
if trace:
|
|
166
|
+
enable_tracing(trace)
|
|
167
|
+
|
|
168
|
+
content: str | list[ContentPart] = prompt
|
|
169
|
+
if files:
|
|
170
|
+
try:
|
|
171
|
+
content = content_parts(prompt, *files)
|
|
172
|
+
except (OSError, ValueError) as e:
|
|
173
|
+
console.print(f"[red]--file: {e}[/red]")
|
|
174
|
+
raise SystemExit(1)
|
|
175
|
+
|
|
141
176
|
client = LLMClient(model=model, base_url=base_url, on_thought=on_thought)
|
|
142
|
-
messages: list[Message] = [Message(role="user", content=
|
|
177
|
+
messages: list[Message] = [Message(role="user", content=content)]
|
|
143
178
|
async with client:
|
|
144
179
|
if stream:
|
|
145
180
|
async for chunk in client.stream_chat(messages, extra_params=parsed_extra):
|
|
@@ -19,6 +19,7 @@ from padwan_llm import RealtimeClient, RealtimeConnection, RealtimeServerEvent
|
|
|
19
19
|
from padwan_llm.errors import LLMError
|
|
20
20
|
from padwan_llm.openai.realtime import NO_TURN_DETECTION, REALTIME_SAMPLE_RATE
|
|
21
21
|
|
|
22
|
+
from .trace import TRACE_BACKENDS, TraceBackend, enable_tracing
|
|
22
23
|
from .utils import console
|
|
23
24
|
|
|
24
25
|
SR = REALTIME_SAMPLE_RATE # 24 kHz mono PCM16, as gpt-realtime expects
|
|
@@ -317,6 +318,12 @@ async def talk_command(
|
|
|
317
318
|
check: bool = Option(
|
|
318
319
|
False, "--check", help="List audio devices and key status, then exit"
|
|
319
320
|
),
|
|
321
|
+
trace: TraceBackend | None = Option(
|
|
322
|
+
None,
|
|
323
|
+
"--trace",
|
|
324
|
+
help="Export LLM telemetry to this backend",
|
|
325
|
+
choices=TRACE_BACKENDS,
|
|
326
|
+
),
|
|
320
327
|
) -> None:
|
|
321
328
|
"""Talk with a real-time voice assistant (speech-to-speech)."""
|
|
322
329
|
# A live mic/speaker session needs the whole terminal; the Textual TUI owns it,
|
|
@@ -343,6 +350,9 @@ async def talk_command(
|
|
|
343
350
|
console.print(f"model={model} voice={voice}")
|
|
344
351
|
return
|
|
345
352
|
|
|
353
|
+
if trace:
|
|
354
|
+
enable_tracing(trace)
|
|
355
|
+
|
|
346
356
|
push_to_talk = not hands_free
|
|
347
357
|
if push_to_talk and not sys.stdin.isatty():
|
|
348
358
|
console.print(
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import os
|
|
5
|
+
from typing import Literal, get_args
|
|
6
|
+
|
|
7
|
+
from .utils import console
|
|
8
|
+
|
|
9
|
+
__all__ = ("TRACE_BACKENDS", "TraceBackend", "enable_tracing")
|
|
10
|
+
|
|
11
|
+
TraceBackend = Literal["langfuse", "otlp"]
|
|
12
|
+
# piou only derives choices from a bare Literal, not `Literal | None`.
|
|
13
|
+
TRACE_BACKENDS: list[TraceBackend] = list(get_args(TraceBackend))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _missing_extra(backend: TraceBackend) -> SystemExit:
|
|
17
|
+
extra = "langfuse" if backend == "langfuse" else "otel"
|
|
18
|
+
console.print(f"[red]{backend} tracing dependencies not installed.[/red]")
|
|
19
|
+
console.print(f"[dim]Install the {extra} extra: `uv sync --extra {extra}`.[/dim]")
|
|
20
|
+
return SystemExit(1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _enable_langfuse() -> None:
|
|
24
|
+
from padwan_llm.langfuse import instrument
|
|
25
|
+
|
|
26
|
+
integration = instrument()
|
|
27
|
+
atexit.register(integration.shutdown)
|
|
28
|
+
console.print("[dim]Tracing enabled (Langfuse)[/dim]")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _enable_otlp() -> None:
|
|
32
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
|
33
|
+
OTLPMetricExporter,
|
|
34
|
+
)
|
|
35
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
36
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
37
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
38
|
+
from opentelemetry.sdk.resources import Resource
|
|
39
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
40
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
41
|
+
|
|
42
|
+
from padwan_llm import otel
|
|
43
|
+
|
|
44
|
+
resource = Resource.create({"service.name": "padwan-cli"})
|
|
45
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
46
|
+
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
|
|
47
|
+
meter_provider = MeterProvider(
|
|
48
|
+
resource=resource,
|
|
49
|
+
metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())],
|
|
50
|
+
)
|
|
51
|
+
otel.instrument(tracer_provider=tracer_provider, meter_provider=meter_provider)
|
|
52
|
+
atexit.register(meter_provider.shutdown)
|
|
53
|
+
atexit.register(tracer_provider.shutdown)
|
|
54
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
|
|
55
|
+
console.print(f"[dim]Tracing enabled (OTLP → {endpoint})[/dim]")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def enable_tracing(backend: TraceBackend) -> None:
|
|
59
|
+
"""Instrument padwan-llm clients for this process, exporting to `backend`.
|
|
60
|
+
|
|
61
|
+
`langfuse` reads the standard `LANGFUSE_*` env vars; `otlp` uses the
|
|
62
|
+
standard `OTEL_EXPORTER_OTLP_*` env vars. Exporters are flushed at exit.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
if backend == "langfuse":
|
|
66
|
+
_enable_langfuse()
|
|
67
|
+
else:
|
|
68
|
+
_enable_otlp()
|
|
69
|
+
except ImportError:
|
|
70
|
+
raise _missing_extra(backend)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
|
|
3
|
+
from padwan_llm 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
|
+
)
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
7
7
|
|
|
8
8
|
from rich import box
|
|
9
9
|
from rich.columns import Columns
|
|
@@ -29,6 +29,9 @@ CHIP_BG = "#1f2230"
|
|
|
29
29
|
CHIP_BORDER = "#3a3f55"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
AttachmentKind = Literal["image", "audio", "text"]
|
|
33
|
+
|
|
34
|
+
|
|
32
35
|
@dataclass
|
|
33
36
|
class Attachment:
|
|
34
37
|
"""A file dropped into the chat, queued to send with the next message."""
|
|
@@ -36,8 +39,8 @@ class Attachment:
|
|
|
36
39
|
path: Path
|
|
37
40
|
name: str
|
|
38
41
|
size: int
|
|
39
|
-
|
|
40
|
-
supported: bool # whether the active model can use it (images need
|
|
42
|
+
kind: AttachmentKind
|
|
43
|
+
supported: bool # whether the active model can use it (images/audio need support)
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
def human_size(num_bytes: int) -> str:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "padwan-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "CLI and TUI for padwan-llm"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"padwan-llm[realtime]>=0.
|
|
8
|
+
"padwan-llm[realtime]>=0.9.0",
|
|
9
9
|
"piou[tui]>=0.36.0",
|
|
10
10
|
"tracktolib>=0.74.0",
|
|
11
11
|
]
|
|
@@ -15,6 +15,12 @@ email = "julien.brayere@polarsen.com"
|
|
|
15
15
|
|
|
16
16
|
[project.optional-dependencies]
|
|
17
17
|
voice = ["sounddevice>=0.5"]
|
|
18
|
+
otel = [
|
|
19
|
+
"padwan-llm[otel]>=0.9.0",
|
|
20
|
+
"opentelemetry-sdk>=1.44",
|
|
21
|
+
"opentelemetry-exporter-otlp>=1.44",
|
|
22
|
+
]
|
|
23
|
+
langfuse = ["padwan-llm[langfuse]>=0.9.0"]
|
|
18
24
|
|
|
19
25
|
[project.scripts]
|
|
20
26
|
padwan-cli = "padwan_cli:main_tui"
|
|
@@ -45,6 +51,9 @@ dev = [
|
|
|
45
51
|
"pytest-asyncio>=1.3.0",
|
|
46
52
|
"pytest-cov>=7.0.0",
|
|
47
53
|
"ruff>=0.11.0",
|
|
54
|
+
"padwan-llm[langfuse]>=0.9.0",
|
|
55
|
+
"opentelemetry-sdk>=1.44",
|
|
56
|
+
"opentelemetry-exporter-otlp>=1.44",
|
|
48
57
|
]
|
|
49
58
|
docs = ["zensical>=0.0.31"]
|
|
50
59
|
bump = ["commitizen>=4.0.0"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "padwan-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "CLI and TUI for padwan-llm"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -8,7 +8,7 @@ authors = [
|
|
|
8
8
|
]
|
|
9
9
|
requires-python = ">=3.14"
|
|
10
10
|
dependencies = [
|
|
11
|
-
"padwan-llm[realtime]>=0.
|
|
11
|
+
"padwan-llm[realtime]>=0.9.0",
|
|
12
12
|
"piou[tui]>=0.36.0",
|
|
13
13
|
"tracktolib>=0.74.0",
|
|
14
14
|
]
|
|
@@ -16,6 +16,14 @@ dependencies = [
|
|
|
16
16
|
[project.optional-dependencies]
|
|
17
17
|
# `talk` command audio I/O; also needs the PortAudio system lib (libportaudio2).
|
|
18
18
|
voice = ["sounddevice>=0.5"]
|
|
19
|
+
# `--trace otlp`: OTel instrumentation with an OTLP exporter.
|
|
20
|
+
otel = [
|
|
21
|
+
"padwan-llm[otel]>=0.9.0",
|
|
22
|
+
"opentelemetry-sdk>=1.44",
|
|
23
|
+
"opentelemetry-exporter-otlp>=1.44",
|
|
24
|
+
]
|
|
25
|
+
# `--trace langfuse`: OTel instrumentation with the Langfuse adapter.
|
|
26
|
+
langfuse = ["padwan-llm[langfuse]>=0.9.0"]
|
|
19
27
|
|
|
20
28
|
[project.scripts]
|
|
21
29
|
padwan-cli = "padwan_cli:main_tui"
|
|
@@ -36,6 +44,10 @@ dev = [
|
|
|
36
44
|
"pytest-asyncio>=1.3.0",
|
|
37
45
|
"pytest-cov>=7.0.0",
|
|
38
46
|
"ruff>=0.11.0",
|
|
47
|
+
# keep the otel and langfuse extras type-checkable/testable in dev
|
|
48
|
+
"padwan-llm[langfuse]>=0.9.0",
|
|
49
|
+
"opentelemetry-sdk>=1.44",
|
|
50
|
+
"opentelemetry-exporter-otlp>=1.44",
|
|
39
51
|
]
|
|
40
52
|
docs = [
|
|
41
53
|
"zensical>=0.0.31",
|
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
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|