AQIT 0.1.0
Loading...
Searching...
No Matches
ink_bridge.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""JSONL bridge for driving the Python chat agent from another process.
3
4stdin (commands): {"type":"user","text":"..."} | {"type":"cancel"} | {"type":"exit"}
5stdout (events): ready | token | tool.start | tool.result | turn.done | error | bye
6"""
7
8from __future__ import annotations
9
10import json
11import os
12import sys
13import threading
14from typing import Any
15
16
17def _emit(event: dict[str, Any]) -> None:
18 sys.stdout.write(json.dumps(event, ensure_ascii=False) + "\n")
19 sys.stdout.flush()
20
22def run_ink_chat_bridge() -> None:
23 """Session loop: load model once, then serve turns until exit/EOF."""
24 from aquin.cli import _BASE_URL, _inject_session_mode
25 from aquin.compute.model_loader import get_active_model_id, load_model, resolve_model_id
26 from aquin.engine.agent import run_agent_turn
27 from aquin.engine.local_server import start as _start_local_server
28 from aquin.engine.main import _load_state, resolve_api_key
29 from aquin.engine.session_memory_store import load_local_memory
30 from aquin.session_mode import get_filtered_tool_schemas, mode_for_model_id, mode_label
31
32 active = (get_active_model_id() or "").strip()
33 if not active:
34 _emit({
35 "type": "error",
36 "message": "No model loaded. Run: aquin load model <id>",
37 "fatal": True,
38 })
39 _emit({"type": "bye"})
40 return
41 try:
42 locked_model = resolve_model_id(active)
43 except ValueError:
44 locked_model = active
45
46 state = _load_state()
47 api_key = state.get("api_key") or resolve_api_key()
48 if not api_key:
49 _emit({
50 "type": "error",
51 "message": "Cloud session not configured for chat.",
52 "fatal": True,
53 })
54 _emit({"type": "bye"})
55 return
56
57 base_url = (state.get("base_url") or os.environ.get("AQUIN_BASE_URL", _BASE_URL)).rstrip("/")
58
59 try:
60 chat_mode = mode_for_model_id(locked_model)
61 if not get_filtered_tool_schemas(chat_mode):
62 _emit({
63 "type": "error",
64 "message": "No tools available for this model mode. Run: aquin load model <id>",
65 "fatal": True,
66 })
67 _emit({"type": "bye"})
68 return
69 except ValueError as exc:
70 _emit({"type": "error", "message": str(exc), "fatal": True})
71 _emit({"type": "bye"})
72 return
73
74 session_state: dict = {
75 "memory": load_local_memory("local"),
76 "activeModelId": locked_model,
77 "session_mode": chat_mode,
78 }
79 session_state = _inject_session_mode(session_state)
80
81 from aquin.compute.loader_shim import apply as _shim_apply
82
83 _shim_apply()
84 try:
85 load_model(locked_model)
86 except Exception as exc:
87 _emit({"type": "error", "message": str(exc), "fatal": True})
88 _emit({"type": "bye"})
89 return
90
91 _start_local_server()
92
93 _emit({
94 "type": "ready",
95 "model": locked_model,
96 "mode": chat_mode,
97 "mode_label": mode_label(chat_mode),
98 })
99
100 cancel_event = threading.Event()
101 turn_lock = threading.Lock()
102
103 def _run_turn(user_text: str) -> None:
104 cancel_event.clear()
105
106 def _on_token(piece: str) -> None:
107 if cancel_event.is_set():
108 raise KeyboardInterrupt()
109 _emit({"type": "token", "text": piece})
110
111 def _on_tool_start(name: str, tool_args: dict) -> None:
112 if cancel_event.is_set():
113 raise KeyboardInterrupt()
114 _emit({"type": "tool.start", "name": name, "args": tool_args or {}})
115
116 def _on_tool_result(name: str, result: Any, is_error: bool) -> None:
117 _ = result
118 _emit({"type": "tool.result", "name": name, "error": bool(is_error)})
119
120 try:
121 run_agent_turn(
122 session_id="",
123 user_message=user_text,
124 state=session_state,
125 api_key=api_key,
126 base_url=base_url,
127 on_token=_on_token,
128 on_tool_start=_on_tool_start,
129 on_tool_result=_on_tool_result,
130 )
131 if cancel_event.is_set():
132 _emit({"type": "error", "message": "Cancelled.", "fatal": False})
133 else:
134 _emit({"type": "turn.done"})
135 except KeyboardInterrupt:
136 _emit({"type": "error", "message": "Cancelled.", "fatal": False})
137 _emit({"type": "turn.done"})
138 except Exception as exc:
139 msg = str(exc)
140 if "401" in msg or "403" in msg:
141 msg = "Auth error. Set required env credentials."
142 elif "connect" in msg.lower() or "connection" in msg.lower() or "network" in msg.lower():
143 msg = "Could not reach Aquin. Check your internet connection."
144 else:
145 msg = "Something went wrong. Try again."
146 _emit({"type": "error", "message": msg, "fatal": False})
147 _emit({"type": "turn.done"})
148
149 for line in sys.stdin:
150 line = line.strip()
151 if not line:
152 continue
153 try:
154 cmd = json.loads(line)
155 except json.JSONDecodeError:
156 _emit({"type": "error", "message": "bad command JSON", "fatal": False})
157 continue
158
159 kind = cmd.get("type")
160 if kind == "exit":
161 break
162 if kind == "cancel":
163 cancel_event.set()
164 continue
165 if kind != "user":
166 _emit({"type": "error", "message": f"unknown command: {kind}", "fatal": False})
167 continue
168
169 text = str(cmd.get("text") or "").strip()
170 if not text:
171 continue
172
173 if not turn_lock.acquire(blocking=False):
174 _emit({"type": "error", "message": "Turn already in progress.", "fatal": False})
175 continue
176
177 try:
178 _run_turn(text)
179 finally:
180 turn_lock.release()
181
182 _emit({"type": "bye"})
None _emit(dict[str, Any] event)
Definition ink_bridge.py:21