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.
@@ -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 .run import cli, main, main_tui
4
2
 
5
3
  __all__ = ("cli", "main", "main_tui")
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  from .run import batch_group
4
2
 
5
3
  __all__ = ["batch_group"]
@@ -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 padwan_llm.gemini import BatchJob
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 padwan_llm import GEMINI_MODELS
10
- from padwan_llm.gemini import BatchRequest, BatchResult, GeminiClient
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 padwan_llm.gemini import BatchRequest, BatchResult
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 padwan_llm import (
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 padwan_llm.gemini import GeminiClient
28
- from padwan_llm.gemini.models import ThinkingConfig
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 `padwan_llm.ConversationStore` protocol so that
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 as a data
105
- URL. Unsupported images (text-only model) and unreadable files are dropped
106
- from the payload — they still appear in the sent badge.
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.is_image:
115
- if a.supported:
116
- parts.append(image_part(a.path))
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
- is_image = (mimetypes.guess_type(p)[0] or "").startswith("image/")
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
- is_image=is_image,
271
- supported=(not is_image) or supports_vision(model),
319
+ kind=kind,
320
+ supported=supported,
272
321
  )
273
322
  )
274
- blind = [a for a in pending if a.is_image and not a.supported]
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(bool(blind))
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
- skipped = [
302
- a for a in sent if a.is_image and not a.supported
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-llm mcp.py).
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 padwan_llm import (
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 padwan_llm._base import OnThought
20
- from padwan_llm.conversation import Message
21
- from padwan_llm.errors import Provider
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) + len(GEMINI_MODELS) + len(MISTRAL_MODELS) + len(GROK_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=prompt)]
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 padwan_llm import RealtimeClient, RealtimeConnection, RealtimeServerEvent
19
- from padwan_llm.errors import LLMError
20
- from padwan_llm.openai.realtime import NO_TURN_DETECTION, REALTIME_SAMPLE_RATE
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 padwan_llm.gemini import BatchJob, BatchResult
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
- is_image: bool
40
- supported: bool # whether the active model can use it (images need vision)
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.5.0"
4
- description = "CLI and TUI for padwan-llm"
3
+ version = "0.6.1"
4
+ description = "CLI and TUI for padwan-ai"
5
5
  readme = "README.md"
6
- requires-python = ">=3.14"
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-llm[realtime]>=0.8.0",
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.com"
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
- ```
@@ -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,7 +0,0 @@
1
- from rich.console import Console
2
-
3
- from padwan_llm import GEMINI_MODELS, GROK_MODELS, MISTRAL_MODELS, OPENAI_MODELS
4
-
5
- console = Console()
6
-
7
- ALL_MODELS = sorted([*OPENAI_MODELS, *GEMINI_MODELS, *MISTRAL_MODELS, *GROK_MODELS])
@@ -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"