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.
@@ -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 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)
@@ -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) + len(GEMINI_MODELS) + len(MISTRAL_MODELS) + len(GROK_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=prompt)]
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
- is_image: bool
40
- supported: bool # whether the active model can use it (images need vision)
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.5.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.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.5.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.8.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
- ```
@@ -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])