AQIT 0.1.0
Loading...
Searching...
No Matches
fallback.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Rich chat REPL used when the Textual TUI cannot (or should not) run."""
3
4from __future__ import annotations
5
6import os
7import sys
8from typing import Any, Literal
9
10from aquin.tui.detect import TuiCapability
11
12
13ChatUi = Literal["tui", "fallback"]
14
15
16def choose_chat_ui(cap: TuiCapability) -> ChatUi:
17 """Pick Textual vs Rich from a detect result."""
18 return "tui" if cap.tui_ok else "fallback"
19
22 args: list[str] | None = None,
23 *,
24 capability: TuiCapability | None = None,
25) -> None:
26 """Run today's Rich ``aquin chat`` REPL (same agent / tools / sync)."""
27 _ = args
28 _ = capability
29
30 # Lazy imports avoid circular load with aquin.cli helpers.
31 from aquin.cli import _BASE_URL, _inject_session_mode, _require_loaded_model_id
32 from aquin.compute.model_loader import load_model
33 from aquin.engine.agent import run_agent_turn
34 from aquin.engine.local_server import start as _start_local_server
35 from aquin.engine.main import _load_state, resolve_api_key
36 from aquin.engine.session_memory_store import load_local_memory
37 from aquin.session_mode import get_filtered_tool_schemas, mode_for_model_id, mode_label
38 from rich.console import Console
39 from rich.prompt import Prompt
40 from rich.rule import Rule
41 from rich.status import Status
42 from rich.text import Text
43 from rich.theme import Theme
44
45 _theme = Theme({
46 "user.label": "bold bright_white",
47 "user.text": "white",
48 "aquin.label": "bold #facc15",
49 "aquin.text": "#e5e7eb",
50 "tool.name": "bold #34d399",
51 "tool.error": "bold #f87171",
52 "tool.args": "dim #9ca3af",
53 "meta": "dim #6b7280",
54 })
55 console = Console(theme=_theme, highlight=False)
56
57 locked_model = _require_loaded_model_id()
58 state = _load_state()
59 api_key = state.get("api_key") or resolve_api_key()
60 base_url = (state.get("base_url") or os.environ.get("AQUIN_BASE_URL", _BASE_URL)).rstrip("/")
61
62 try:
63 chat_mode = mode_for_model_id(locked_model)
64 if not get_filtered_tool_schemas(chat_mode):
65 console.print(
66 "[tool.error]No tools available for this model mode.[/]\n"
67 "Run: aquin load model <model-id>",
68 )
69 sys.exit(1)
70 except ValueError as exc:
71 console.print(f"[tool.error]{exc}[/]")
72 console.print("[meta]Run: aquin load model <model-id>[/]")
73 sys.exit(1)
74
75 session_state: dict = {
76 "memory": load_local_memory("local"),
77 "activeModelId": locked_model,
78 "session_mode": chat_mode,
79 }
80 session_state = _inject_session_mode(session_state)
81
82 from aquin.compute.loader_shim import apply as _shim_apply
83
84 _shim_apply()
85 try:
86 load_model(locked_model)
87 except Exception as exc:
88 console.print(f"[tool.error]{exc}[/]")
89 sys.exit(1)
90
91 _start_local_server()
92
93 console.print()
94 console.print(f"[meta]{mode_label(chat_mode)}[/] [meta]{locked_model}[/]")
95 console.print(Rule("[aquin.label]aquin chat[/]", style="dim #374151"))
96 console.print("[meta]Ctrl+C or empty line to exit[/]\n")
97
98 try:
99 while True:
100 try:
101 user_input = Prompt.ask("[user.label]you[/]", console=console).strip()
102 except (EOFError, KeyboardInterrupt):
103 console.print()
104 break
105
106 if not user_input:
107 break
108
109 _first_token = True
110 _spinner_active = [True]
111 _status = Status(" thinking...", console=console, spinner="dots")
112 _status.start()
113
114 def _stop_spinner() -> None:
115 if _spinner_active[0]:
116 _spinner_active[0] = False
117 _status.stop()
118
119 def _on_token(piece: str) -> None:
120 nonlocal _first_token
121 _stop_spinner()
122 if _first_token:
123 console.print(Text("aquin ", style="aquin.label"), end="")
124 _first_token = False
125 sys.stdout.write(piece)
126 sys.stdout.flush()
127
128 def _on_tool_start(name: str, tool_args: dict) -> None:
129 nonlocal _first_token
130 _ = tool_args
131 _stop_spinner()
132 if not _first_token:
133 console.print()
134 _first_token = True
135 console.print(Text.assemble(
136 Text(" [tool] ", style="meta"),
137 Text(name, style="tool.name"),
138 Text(" running...", style="meta"),
139 ))
140
141 def _on_tool_result(name: str, result: Any, is_error: bool) -> None:
142 _ = result
143 style = "tool.error" if is_error else "tool.name"
144 suffix = " error" if is_error else " done"
145 console.print(Text.assemble(
146 Text(" [tool] ", style="meta"),
147 Text(name, style=style),
148 Text(suffix, style="meta"),
149 ))
150
151 try:
152 run_agent_turn(
153 session_id="",
154 user_message=user_input,
155 state=session_state,
156 api_key=api_key,
157 base_url=base_url,
158 on_token=_on_token,
159 on_tool_start=_on_tool_start,
160 on_tool_result=_on_tool_result,
161 )
162 except KeyboardInterrupt:
163 _stop_spinner()
164 console.print()
165 raise
166 except Exception as exc:
167 _stop_spinner()
168 msg = str(exc)
169 if "401" in msg or "403" in msg:
170 console.print(Text("\n Auth error. Set HF_TOKEN / required env credentials.", style="tool.error"))
171 elif "connect" in msg.lower() or "connection" in msg.lower() or "network" in msg.lower():
172 console.print(Text("\n Could not reach Aquin. Check your internet connection.", style="tool.error"))
173 else:
174 console.print(Text("\n Something went wrong. Try again.", style="tool.error"))
175 finally:
176 _stop_spinner()
177
178 console.print()
179
180 except KeyboardInterrupt:
181 console.print()
None run_chat_fallback(list[str]|None args=None, *, TuiCapability|None capability=None)
Definition fallback.py:29
ChatUi choose_chat_ui(TuiCapability cap)
Definition fallback.py:20