236 lines
6.7 KiB
Python
236 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Iterable, Iterator
|
|
|
|
import anyio
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.text import Text
|
|
|
|
from takopi.config import ConfigError
|
|
from takopi.telegram import onboarding as ob
|
|
from takopi.telegram.api_models import User
|
|
|
|
|
|
def section(console: Console, title: str) -> None:
|
|
console.print("")
|
|
console.print(f"=== {title} ===", markup=False)
|
|
|
|
|
|
def render_confirm(console: Console, prompt: str) -> None:
|
|
console.print(f"? {prompt} (yes/no)", markup=False)
|
|
|
|
|
|
def render_password(console: Console, prompt: str) -> None:
|
|
console.print(f"? {prompt} {'*' * 28}", markup=False)
|
|
|
|
|
|
def render_select(console: Console, prompt: str, choices: list[str]) -> None:
|
|
console.print(f"? {prompt} (use arrow keys)", markup=False)
|
|
for index, choice in enumerate(choices):
|
|
marker = ">" if index == 0 else " "
|
|
console.print(f"{marker} {choice}", markup=False)
|
|
|
|
|
|
def next_value(values: Iterator[Any], label: str) -> Any:
|
|
try:
|
|
return next(values)
|
|
except StopIteration as exc:
|
|
raise RuntimeError(f"scripted ui ran out of {label} responses") from exc
|
|
|
|
|
|
class ScriptedUI:
|
|
def __init__(
|
|
self,
|
|
console: Console,
|
|
*,
|
|
confirms: Iterable[bool | None],
|
|
selects: Iterable[Any],
|
|
passwords: Iterable[str | None],
|
|
) -> None:
|
|
self._console = console
|
|
self._confirms = iter(confirms)
|
|
self._selects = iter(selects)
|
|
self._passwords = iter(passwords)
|
|
|
|
@property
|
|
def console(self) -> Console:
|
|
return self._console
|
|
|
|
def panel(
|
|
self,
|
|
title: str | None,
|
|
body: str,
|
|
*,
|
|
border_style: str = "yellow",
|
|
) -> None:
|
|
panel = Panel(
|
|
body,
|
|
title=title,
|
|
border_style=border_style,
|
|
padding=(1, 2),
|
|
expand=False,
|
|
)
|
|
self._console.print(panel)
|
|
|
|
def step(self, title: str, *, number: int) -> None:
|
|
self._console.print("")
|
|
self._console.print(Text(f"step {number}: {title}", style="bold yellow"))
|
|
self._console.print("")
|
|
|
|
def print(self, text: object = "", *, markup: bool | None = None) -> None:
|
|
if markup is None:
|
|
self._console.print(text)
|
|
return
|
|
self._console.print(text, markup=markup)
|
|
|
|
async def confirm(self, prompt: str, default: bool = True) -> bool | None:
|
|
render_confirm(self._console, prompt)
|
|
return next_value(self._confirms, "confirm")
|
|
|
|
async def select(self, prompt: str, choices: list[tuple[str, Any]]) -> Any | None:
|
|
rendered = [label for label, _value in choices]
|
|
render_select(self._console, prompt, rendered)
|
|
return next_value(self._selects, "select")
|
|
|
|
async def password(self, prompt: str) -> str | None:
|
|
render_password(self._console, prompt)
|
|
return next_value(self._passwords, "password")
|
|
|
|
|
|
@dataclass
|
|
class ScriptedServices:
|
|
bot: User
|
|
chat: ob.ChatInfo
|
|
engines: list[tuple[str, bool, str | None]]
|
|
topics_issue: ConfigError | None = None
|
|
existing_config: dict[str, Any] | None = None
|
|
written_config: dict[str, Any] | None = None
|
|
|
|
async def get_bot_info(self, _token: str) -> User | None:
|
|
return self.bot
|
|
|
|
async def wait_for_chat(self, _token: str) -> ob.ChatInfo:
|
|
return self.chat
|
|
|
|
async def validate_topics(
|
|
self, _token: str, _chat_id: int, _scope: ob.TopicScope
|
|
) -> ConfigError | None:
|
|
return self.topics_issue
|
|
|
|
def list_engines(self) -> list[tuple[str, bool, str | None]]:
|
|
return self.engines
|
|
|
|
def read_config(self, _path) -> dict[str, Any]:
|
|
return dict(self.existing_config or {})
|
|
|
|
def write_config(self, _path, data: dict[str, Any]) -> None:
|
|
self.written_config = data
|
|
|
|
|
|
async def run_flow(title: str, ui: ScriptedUI, svc: ScriptedServices) -> None:
|
|
section(ui.console, title)
|
|
state = ob.OnboardingState(config_path=ob.HOME_CONFIG_PATH, force=False)
|
|
await ob.run_onboarding(ui, svc, state)
|
|
|
|
|
|
def main() -> None:
|
|
console = Console()
|
|
|
|
bot = User(id=1, username="bunny_agent_bot", first_name="Bunny")
|
|
group_chat = ob.ChatInfo(
|
|
chat_id=-1001234567890,
|
|
username=None,
|
|
title="takopi devs",
|
|
first_name=None,
|
|
last_name=None,
|
|
chat_type="supergroup",
|
|
)
|
|
private_chat = ob.ChatInfo(
|
|
chat_id=462722,
|
|
username="banteg",
|
|
title=None,
|
|
first_name="Banteg",
|
|
last_name=None,
|
|
chat_type="private",
|
|
)
|
|
engines_installed = [
|
|
("codex", True, "brew install codex"),
|
|
("claude", True, "brew install claude"),
|
|
("opencode", False, "brew install opencode"),
|
|
]
|
|
engines_missing = [
|
|
("codex", False, "brew install codex"),
|
|
("claude", False, "brew install claude"),
|
|
("opencode", False, "brew install opencode"),
|
|
]
|
|
|
|
anyio.run(
|
|
run_flow,
|
|
"assistant mode (private chat)",
|
|
ScriptedUI(
|
|
console,
|
|
confirms=[True, True],
|
|
selects=["assistant", "codex"],
|
|
passwords=["123456789:ABCdef"],
|
|
),
|
|
ScriptedServices(bot=bot, chat=private_chat, engines=engines_installed),
|
|
)
|
|
|
|
anyio.run(
|
|
run_flow,
|
|
"handoff mode (token instructions)",
|
|
ScriptedUI(
|
|
console,
|
|
confirms=[False, True],
|
|
selects=["handoff", "codex"],
|
|
passwords=["123456789:ABCdef"],
|
|
),
|
|
ScriptedServices(bot=bot, chat=private_chat, engines=engines_installed),
|
|
)
|
|
|
|
anyio.run(
|
|
run_flow,
|
|
"workspace mode (topics)",
|
|
ScriptedUI(
|
|
console,
|
|
confirms=[True, True],
|
|
selects=["workspace", "codex"],
|
|
passwords=["123456789:ABCdef"],
|
|
),
|
|
ScriptedServices(bot=bot, chat=group_chat, engines=engines_installed),
|
|
)
|
|
|
|
anyio.run(
|
|
run_flow,
|
|
"topics validation warning",
|
|
ScriptedUI(
|
|
console,
|
|
confirms=[True, True],
|
|
selects=["workspace", "assistant", "codex"],
|
|
passwords=["123456789:ABCdef"],
|
|
),
|
|
ScriptedServices(
|
|
bot=bot,
|
|
chat=group_chat,
|
|
engines=engines_installed,
|
|
topics_issue=ConfigError("bot is missing admin rights"),
|
|
),
|
|
)
|
|
|
|
anyio.run(
|
|
run_flow,
|
|
"no engines installed",
|
|
ScriptedUI(
|
|
console,
|
|
confirms=[True, False],
|
|
selects=["assistant"],
|
|
passwords=["123456789:ABCdef"],
|
|
),
|
|
ScriptedServices(bot=bot, chat=private_chat, engines=engines_missing),
|
|
)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|