-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcli_chat.py
More file actions
177 lines (159 loc) · 6.49 KB
/
cli_chat.py
File metadata and controls
177 lines (159 loc) · 6.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Terminal chat for bare ``openprogram``.
Welcome banner (tools + skills inventory) followed by a chat loop.
Slash commands (``/help``, ``/web``, ``/quit``, ...) are handled
locally; non-slash input goes through the same chat runtime the Web UI
uses, so behaviour stays aligned.
The bulk of this module's logic — banner inventory, slash-command
handlers, per-turn exec — lives in ``openprogram/_cli_chat/`` and is
re-exported here so existing call sites (``_timing.py``,
``openprogram.setup``, ``openprogram._cli_cmds.chat``, tests) keep
working unchanged.
"""
from __future__ import annotations
import os
import sys
# Re-exports — every external caller imports through ``openprogram.cli_chat``.
from openprogram._cli_chat.setup import ( # noqa: E402,F401
_get_chat_runtime,
_reset_provider_cache,
_prompt_first_run_setup,
)
from openprogram._cli_chat.banner import ( # noqa: E402,F401
_tool_inventory,
_skill_inventory,
_function_inventory,
_application_inventory,
_section_text,
_print_banner,
)
from openprogram._cli_chat.handlers import ( # noqa: E402,F401
SLASH_HELP,
_parse_kv_args,
_handle_slash,
_handle_login,
_handle_model,
_handle_agent_switch,
_handle_new_session,
_handle_copy,
_handle_attach,
_handle_detach,
_handle_connections,
)
from openprogram._cli_chat.turn import _run_turn_with_history # noqa: E402,F401
def run_cli_chat(oneshot: str | None = None,
resume: str | None = None,
tui: bool = True) -> None:
"""Launch the terminal chat.
``oneshot`` runs one turn and exits (still persisted so it shows
up in the sidebar of a later Web UI session).
``resume`` picks up a prior session id under the current default
agent instead of starting a fresh one.
``tui`` defaults True on macOS / Linux and False on Windows (Ink
can't enter raw input mode on Windows consoles). Callers don't
need to pass it — ``openprogram`` resolves it from platform.
``oneshot`` always uses the Rich path (one-shot doesn't render
a TUI).
"""
import uuid as _uuid
from rich.console import Console
from openprogram.agents import manager as _A
console = Console()
# Provider detection probes 5+ providers (CLI binaries + API hosts)
# on cold cache; that takes several seconds. Tell the user something
# is happening so the TUI launch doesn't look frozen.
with console.status("Detecting providers…", spinner="dots"):
provider, rt = _get_chat_runtime()
if rt is None:
if not _prompt_first_run_setup(console):
sys.exit(1)
provider, rt = _get_chat_runtime()
if rt is None:
sys.exit(1)
model = getattr(rt, "model", "?")
agent = _A.get_default()
if agent is None:
agent = _A.create("main", make_default=True)
if resume:
session_id = resume
else:
session_id = "local_" + _uuid.uuid4().hex[:10]
# Full-screen TUI path (default). One-shot stays on the Rich path
# because rendering a scroll buffer for a single turn is overkill.
if tui and not oneshot:
try:
from openprogram.cli_ink import run_ink_tui
run_ink_tui(agent=agent, session_id=session_id, rt=rt)
return
except Exception as e: # noqa: BLE001
# cli.py:_maybe_redirect_for_tui() already dup2'd stdout/stderr
# to ~/.openprogram/logs/ink-startup.log on the assumption the
# Ink TUI would take over the terminal. The fallback REPL writes
# to those same fds, so without restoring them the user sees a
# frozen blank terminal while everything goes to the log file.
from openprogram import cli as _cli
for std_fd, saved_attr in ((1, "_TUI_TTY_OUT"), (2, "_TUI_TTY_ERR")):
saved = getattr(_cli, saved_attr, None)
if saved is not None:
try:
os.dup2(saved, std_fd)
except OSError:
pass
console.print(
f"[yellow]TUI failed to start ({type(e).__name__}: {e}); "
f"falling back to REPL.[/]"
)
# Rich REPL fallback / oneshot path. For the chat REPL, the banner
# below already shows agent + session — no need for a separate
# "New session ..." line above it. For ``--print`` / oneshot, skip
# the banner entirely and just print the reply; the user wanted a
# quick answer, not a UI.
if oneshot:
reply = _run_turn_with_history(agent, session_id, oneshot)
print(reply)
return
if resume:
console.print(f"[dim]↪ Resuming previous session.[/]")
# Show the channels worker status without asking the user to start
# anything: the primary thing this REPL does is chat. Channels are
# an opt-in feature; we surface the status only if a worker is
# already running so the user knows their bindings are live.
try:
from openprogram.worker import current_worker_pid
pid = current_worker_pid()
if pid:
console.print(
f"[dim]↪ channels worker running (PID {pid}) "
f"— bindings active (attach/detach in the Web UI)[/]"
)
except Exception:
pass
_print_banner(console, provider, model,
agent_id=getattr(agent, "id", "") or "",
session_id=session_id)
while True:
try:
user_input = console.input("\n[bold bright_blue]❯[/] ").strip()
except (EOFError, KeyboardInterrupt):
console.print("\n[dim]Goodbye.[/]")
return
if not user_input:
continue
if user_input.startswith("/"):
if _handle_slash(user_input, console, rt,
agent=agent, session_id=session_id):
return
continue
# Stream the reply token-by-token to the terminal. The turn
# function writes directly to stdout via ``rt.on_stream``; we
# don't print again after it returns or the text would
# duplicate. The returned ``reply`` is the canonical full
# string — passed to TTS which needs the whole utterance.
reply = _run_turn_with_history(
agent, session_id, user_input, console=console,
)
# Fire-and-forget TTS; no-ops unless tts.provider is set.
try:
from openprogram.tts import speak
speak(reply)
except Exception:
pass