tech-envision commited on
Commit
5cbac45
·
1 Parent(s): 22fb7d5

Add Windows CLI and session info API

Browse files
Files changed (7) hide show
  1. README.md +11 -0
  2. cli_app/README.md +31 -0
  3. cli_app/__init__.py +0 -0
  4. cli_app/__main__.py +4 -0
  5. cli_app/main.py +88 -0
  6. src/api.py +10 -2
  7. src/db.py +23 -0
README.md CHANGED
@@ -156,3 +156,14 @@ python -m src.cli --user yourname
156
  The tool lists your existing chat sessions and lets you select one or create a
157
  new session. Type messages and the assistant's streamed replies will appear
158
  immediately. Enter ``exit`` or press ``Ctrl+D`` to quit.
 
 
 
 
 
 
 
 
 
 
 
 
156
  The tool lists your existing chat sessions and lets you select one or create a
157
  new session. Type messages and the assistant's streamed replies will appear
158
  immediately. Enter ``exit`` or press ``Ctrl+D`` to quit.
159
+
160
+ ### Windows executable
161
+
162
+ For a standalone application that does not require Python, use the code in
163
+ `cli_app`. After installing ``pyinstaller`` run:
164
+
165
+ ```bash
166
+ pyinstaller --onefile -n llm-chat cli_app/main.py
167
+ ```
168
+
169
+ The resulting ``llm-chat.exe`` can be used on Windows 10/11.
cli_app/README.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Windows CLI Application
2
+
3
+ This folder contains a standalone command line interface for interacting with
4
+ `llm-backend`. The application uses [Typer](https://typer.tiangolo.com/) for a
5
+ simple user experience and can be packaged into a single Windows executable
6
+ using [PyInstaller](https://www.pyinstaller.org/).
7
+
8
+ ## Running from source
9
+
10
+ ```bash
11
+ python -m cli_app --user yourname
12
+ ```
13
+
14
+ ## Building the executable
15
+
16
+ 1. Install PyInstaller:
17
+
18
+ ```bash
19
+ pip install pyinstaller
20
+ ```
21
+
22
+ 2. Build the app:
23
+
24
+ ```bash
25
+ pyinstaller --onefile -n llm-chat cli_app/main.py
26
+ ```
27
+
28
+ The resulting `llm-chat.exe` will appear in the `dist` directory.
29
+
30
+ The executable can be distributed on Windows 10/11 systems without requiring a
31
+ Python installation.
cli_app/__init__.py ADDED
File without changes
cli_app/__main__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
cli_app/main.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import AsyncIterator, List, Dict
5
+
6
+ import httpx
7
+ import typer
8
+ from colorama import Fore, Style, init
9
+
10
+ API_URL = "http://localhost:8000"
11
+
12
+ app = typer.Typer(add_completion=False, help="Chat with the LLM backend API")
13
+
14
+
15
+ async def _get_sessions_info(user: str, server: str) -> List[Dict[str, str]]:
16
+ async with httpx.AsyncClient(base_url=server) as client:
17
+ resp = await client.get(f"/sessions/{user}/info")
18
+ if resp.status_code == 404:
19
+ # Fallback to basic list for older API versions
20
+ resp = await client.get(f"/sessions/{user}")
21
+ resp.raise_for_status()
22
+ names = resp.json().get("sessions", [])
23
+ return [{"name": n, "last_message": ""} for n in names]
24
+ resp.raise_for_status()
25
+ return resp.json().get("sessions", [])
26
+
27
+
28
+ async def _stream_chat(
29
+ user: str, session: str, prompt: str, server: str
30
+ ) -> AsyncIterator[str]:
31
+ async with httpx.AsyncClient(base_url=server, timeout=None) as client:
32
+ async with client.stream(
33
+ "POST",
34
+ "/chat/stream",
35
+ json={"user": user, "session": session, "prompt": prompt},
36
+ ) as resp:
37
+ resp.raise_for_status()
38
+ async for line in resp.aiter_lines():
39
+ if line:
40
+ yield line
41
+
42
+
43
+ async def _chat_loop(user: str, server: str) -> None:
44
+ init(autoreset=True)
45
+ sessions = await _get_sessions_info(user, server)
46
+ session = "default"
47
+ if sessions:
48
+ typer.echo("Existing sessions:")
49
+ for idx, info in enumerate(sessions, 1):
50
+ snippet = f" - {info['last_message'][:40]}" if info.get("last_message") else ""
51
+ typer.echo(f" {idx}. {info['name']}{snippet}")
52
+ choice = typer.prompt(
53
+ "Select session number or enter new name", default=str(len(sessions))
54
+ )
55
+ if choice.isdigit() and 1 <= int(choice) <= len(sessions):
56
+ session = sessions[int(choice) - 1]["name"]
57
+ else:
58
+ session = choice.strip() or session
59
+ else:
60
+ session = typer.prompt("Session name", default=session)
61
+
62
+ typer.echo(
63
+ f"Chatting as {Fore.GREEN}{user}{Style.RESET_ALL} in session '{session}'"
64
+ )
65
+
66
+ while True:
67
+ try:
68
+ msg = typer.prompt(f"{Fore.CYAN}You{Style.RESET_ALL}")
69
+ except EOFError:
70
+ break
71
+ if msg.strip().lower() in {"exit", "quit"}:
72
+ break
73
+ async for part in _stream_chat(user, session, msg, server):
74
+ typer.echo(f"{Fore.YELLOW}{part}{Style.RESET_ALL}")
75
+
76
+
77
+ @app.callback(invoke_without_command=True)
78
+ def main(
79
+ user: str = typer.Option("default", "--user", "-u"),
80
+ server: str = typer.Option(API_URL, "--server", "-s"),
81
+ ) -> None:
82
+ """Start an interactive chat session."""
83
+
84
+ asyncio.run(_chat_loop(user, server))
85
+
86
+
87
+ if __name__ == "__main__": # pragma: no cover - manual execution
88
+ app()
src/api.py CHANGED
@@ -1,7 +1,8 @@
1
  from __future__ import annotations
2
 
3
  from fastapi import FastAPI, UploadFile, File, Form
4
- from fastapi.responses import StreamingResponse
 
5
  from pydantic import BaseModel
6
  import asyncio
7
  import os
@@ -10,7 +11,7 @@ from pathlib import Path
10
 
11
  from .chat import ChatSession
12
  from .log import get_logger
13
- from .db import list_sessions
14
 
15
 
16
  _LOG = get_logger(__name__)
@@ -63,6 +64,13 @@ def create_app() -> FastAPI:
63
  async def list_user_sessions(user: str):
64
  return {"sessions": list_sessions(user)}
65
 
 
 
 
 
 
 
 
66
  @app.get("/health")
67
  async def health():
68
  return {"status": "ok"}
 
1
  from __future__ import annotations
2
 
3
  from fastapi import FastAPI, UploadFile, File, Form
4
+ from fastapi.responses import StreamingResponse, Response
5
+ from fastapi import HTTPException
6
  from pydantic import BaseModel
7
  import asyncio
8
  import os
 
11
 
12
  from .chat import ChatSession
13
  from .log import get_logger
14
+ from .db import list_sessions, list_sessions_info
15
 
16
 
17
  _LOG = get_logger(__name__)
 
64
  async def list_user_sessions(user: str):
65
  return {"sessions": list_sessions(user)}
66
 
67
+ @app.get("/sessions/{user}/info")
68
+ async def list_user_sessions_info(user: str):
69
+ data = list_sessions_info(user)
70
+ if not data:
71
+ raise HTTPException(status_code=404, detail="User not found")
72
+ return {"sessions": data}
73
+
74
  @app.get("/health")
75
  async def health():
76
  return {"status": "ok"}
src/db.py CHANGED
@@ -62,6 +62,7 @@ __all__ = [
62
  "Document",
63
  "reset_history",
64
  "list_sessions",
 
65
  "add_document",
66
  ]
67
 
@@ -110,3 +111,25 @@ def list_sessions(username: str) -> list[str]:
110
  except User.DoesNotExist:
111
  return []
112
  return [c.session_name for c in Conversation.select().where(Conversation.user == user)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  "Document",
63
  "reset_history",
64
  "list_sessions",
65
+ "list_sessions_info",
66
  "add_document",
67
  ]
68
 
 
111
  except User.DoesNotExist:
112
  return []
113
  return [c.session_name for c in Conversation.select().where(Conversation.user == user)]
114
+
115
+
116
+ def list_sessions_info(username: str) -> list[dict[str, str]]:
117
+ """Return session names and a snippet of the last message for ``username``."""
118
+
119
+ init_db()
120
+ try:
121
+ user = User.get(User.username == username)
122
+ except User.DoesNotExist:
123
+ return []
124
+
125
+ sessions = []
126
+ for conv in Conversation.select().where(Conversation.user == user):
127
+ last_msg = (
128
+ Message.select()
129
+ .where(Message.conversation == conv)
130
+ .order_by(Message.created_at.desc())
131
+ .first()
132
+ )
133
+ snippet = (last_msg.content[:50] + "…") if last_msg else ""
134
+ sessions.append({"name": conv.session_name, "last_message": snippet})
135
+ return sessions