virtuai-cli 0.3.0__tar.gz → 0.4.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.
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/PKG-INFO +1 -1
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/pyproject.toml +1 -1
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/__init__.py +1 -1
- virtuai_cli-0.4.0/src/virtuai_cli/chat/history.py +44 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/chat/tui.py +77 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli.egg-info/SOURCES.txt +1 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/README.md +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/setup.cfg +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/chat/__init__.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/chat/command.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/chat/sse.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/chat/widgets.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/config.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/executor.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/main.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/runner.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli/security.py +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli.egg-info/requires.txt +0 -0
- {virtuai_cli-0.3.0 → virtuai_cli-0.4.0}/src/virtuai_cli.egg-info/top_level.txt +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""VirtuAI local CLI."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""HTTP helpers for browsing past conversations from the TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import certifi
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_VERIFY = certifi.where()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def list_conversations(
|
|
15
|
+
server_url: str,
|
|
16
|
+
token: str,
|
|
17
|
+
agent_id: str,
|
|
18
|
+
limit: int = 20,
|
|
19
|
+
) -> list[dict]:
|
|
20
|
+
"""Return up to `limit` of the user's recent conversations for this agent."""
|
|
21
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
22
|
+
url = f"{server_url.rstrip('/')}/api/web-chat/conversations/{agent_id}"
|
|
23
|
+
async with httpx.AsyncClient(verify=_VERIFY, timeout=15.0) as client:
|
|
24
|
+
resp = await client.get(url, headers=headers, params={"limit": limit, "offset": 0})
|
|
25
|
+
resp.raise_for_status()
|
|
26
|
+
return resp.json()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def load_conversation(
|
|
30
|
+
server_url: str,
|
|
31
|
+
token: str,
|
|
32
|
+
agent_id: str,
|
|
33
|
+
session_id: str,
|
|
34
|
+
message_limit: int = 200,
|
|
35
|
+
) -> Optional[dict]:
|
|
36
|
+
"""Return {conversation, messages} for one session_id, or None if not found."""
|
|
37
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
38
|
+
url = f"{server_url.rstrip('/')}/api/web-chat/conversations/{agent_id}/{session_id}"
|
|
39
|
+
async with httpx.AsyncClient(verify=_VERIFY, timeout=30.0) as client:
|
|
40
|
+
resp = await client.get(url, headers=headers, params={"message_limit": message_limit})
|
|
41
|
+
if resp.status_code == 404:
|
|
42
|
+
return None
|
|
43
|
+
resp.raise_for_status()
|
|
44
|
+
return resp.json()
|
|
@@ -14,6 +14,7 @@ from textual.reactive import reactive
|
|
|
14
14
|
from textual.widgets import Footer, Header, Input, Static
|
|
15
15
|
|
|
16
16
|
from virtuai_cli import runner as ws_runner
|
|
17
|
+
from virtuai_cli.chat.history import list_conversations, load_conversation
|
|
17
18
|
from virtuai_cli.chat.sse import stream_chat
|
|
18
19
|
from virtuai_cli.chat.widgets import AssistantTurn, UserBubble
|
|
19
20
|
|
|
@@ -173,11 +174,17 @@ class ChatApp(App):
|
|
|
173
174
|
await self._show_models()
|
|
174
175
|
elif head == "/model":
|
|
175
176
|
await self._set_model(arg)
|
|
177
|
+
elif head == "/history":
|
|
178
|
+
await self._show_history()
|
|
179
|
+
elif head == "/load":
|
|
180
|
+
await self._load_session(arg)
|
|
176
181
|
elif head == "/help":
|
|
177
182
|
await self._append(Static(
|
|
178
183
|
"[b]Commands[/b]\n"
|
|
179
184
|
" /help this list\n"
|
|
180
185
|
" /clear, /new start a fresh conversation\n"
|
|
186
|
+
" /history list this agent's recent conversations\n"
|
|
187
|
+
" /load <id> load a past conversation by session_id\n"
|
|
181
188
|
" /models list models available for this agent\n"
|
|
182
189
|
" /model <id> switch the model for the next message\n"
|
|
183
190
|
" /exit, /quit close the TUI\n"
|
|
@@ -201,6 +208,76 @@ class ChatApp(App):
|
|
|
201
208
|
lines.append("\n[dim]Use [/dim][b]/model <id>[/b][dim] to switch.[/dim]")
|
|
202
209
|
await self._append(Static("\n".join(lines)))
|
|
203
210
|
|
|
211
|
+
async def _show_history(self) -> None:
|
|
212
|
+
try:
|
|
213
|
+
convs = await list_conversations(self.server_url, self.token, self.agent_id, limit=20)
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
await self._append(Static(f"[red]Could not load history:[/red] {exc}"))
|
|
216
|
+
return
|
|
217
|
+
if not convs:
|
|
218
|
+
await self._append(Static("[dim]No past conversations with this agent.[/dim]"))
|
|
219
|
+
return
|
|
220
|
+
lines = ["[b]Recent conversations[/b] [dim](most recent first)[/dim]"]
|
|
221
|
+
for c in convs:
|
|
222
|
+
sid = c.get("session_id", "")
|
|
223
|
+
title = c.get("title") or "(untitled)"
|
|
224
|
+
when = (c.get("last_message_at") or c.get("updated_at") or "")[:19]
|
|
225
|
+
count = c.get("message_count", 0)
|
|
226
|
+
marker = "[green]✓[/green] " if sid == self.session_id else " "
|
|
227
|
+
lines.append(f"{marker}[bold]{sid}[/bold] {when} · {count} msgs · {title}")
|
|
228
|
+
lines.append("\n[dim]Use [/dim][b]/load <session_id>[/b][dim] to switch.[/dim]")
|
|
229
|
+
await self._append(Static("\n".join(lines)))
|
|
230
|
+
|
|
231
|
+
async def _load_session(self, session_id: str) -> None:
|
|
232
|
+
if not session_id:
|
|
233
|
+
await self._append(Static(
|
|
234
|
+
"Usage: [b]/load <session_id>[/b] (see [b]/history[/b] for IDs)"
|
|
235
|
+
))
|
|
236
|
+
return
|
|
237
|
+
if self._stream_task and not self._stream_task.done():
|
|
238
|
+
await self._append(Static(
|
|
239
|
+
"[yellow]Cancel the in-flight response (Esc) before loading another conversation.[/yellow]"
|
|
240
|
+
))
|
|
241
|
+
return
|
|
242
|
+
try:
|
|
243
|
+
data = await load_conversation(self.server_url, self.token, self.agent_id, session_id)
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
await self._append(Static(f"[red]Could not load conversation:[/red] {exc}"))
|
|
246
|
+
return
|
|
247
|
+
if data is None:
|
|
248
|
+
await self._append(Static(f"[red]Conversation not found:[/red] {session_id}"))
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Clear the screen and rebuild it with the loaded history.
|
|
252
|
+
scroll = self.query_one("#conversation", VerticalScroll)
|
|
253
|
+
for child in list(scroll.children):
|
|
254
|
+
await child.remove()
|
|
255
|
+
|
|
256
|
+
conv = data.get("conversation") or {}
|
|
257
|
+
messages = data.get("messages") or []
|
|
258
|
+
self.session_id = conv.get("session_id") or session_id
|
|
259
|
+
self._current_turn = None
|
|
260
|
+
|
|
261
|
+
header = (
|
|
262
|
+
f"[b]Loaded conversation[/b] {self.session_id}\n"
|
|
263
|
+
f"[dim]{conv.get('title') or '(untitled)'} · {len(messages)} message(s)[/dim]"
|
|
264
|
+
)
|
|
265
|
+
await scroll.mount(Static(header))
|
|
266
|
+
|
|
267
|
+
for msg in messages:
|
|
268
|
+
mtype = (msg.get("message_type") or "").lower()
|
|
269
|
+
content = msg.get("content") or ""
|
|
270
|
+
if mtype in ("user", "human"):
|
|
271
|
+
await scroll.mount(UserBubble(content))
|
|
272
|
+
else:
|
|
273
|
+
turn = AssistantTurn()
|
|
274
|
+
await scroll.mount(turn)
|
|
275
|
+
if content:
|
|
276
|
+
await turn.append_token(content)
|
|
277
|
+
turn.mark_final()
|
|
278
|
+
scroll.scroll_end(animate=False)
|
|
279
|
+
self._set_status(f"loaded {self.session_id}")
|
|
280
|
+
|
|
204
281
|
async def _set_model(self, requested: str) -> None:
|
|
205
282
|
if not requested:
|
|
206
283
|
current = self.current_model_id or "(none)"
|
|
@@ -14,6 +14,7 @@ src/virtuai_cli.egg-info/requires.txt
|
|
|
14
14
|
src/virtuai_cli.egg-info/top_level.txt
|
|
15
15
|
src/virtuai_cli/chat/__init__.py
|
|
16
16
|
src/virtuai_cli/chat/command.py
|
|
17
|
+
src/virtuai_cli/chat/history.py
|
|
17
18
|
src/virtuai_cli/chat/sse.py
|
|
18
19
|
src/virtuai_cli/chat/tui.py
|
|
19
20
|
src/virtuai_cli/chat/widgets.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|