tech-envision commited on
Commit
bf3a897
Β·
1 Parent(s): c70c66d

Add interactive CLI and session listing endpoint

Browse files
Files changed (5) hide show
  1. README.md +14 -0
  2. requirements.txt +2 -0
  3. src/api.py +5 -0
  4. src/cli.py +83 -0
  5. src/db.py +12 -0
README.md CHANGED
@@ -134,6 +134,7 @@ uvicorn src.api:app --host 0.0.0.0 --port 8000
134
 
135
  - ``POST /chat/stream`` – Stream the assistant's response as plain text.
136
  - ``POST /upload`` – Upload a document so it can be referenced in chats.
 
137
 
138
  Example request:
139
 
@@ -142,3 +143,16 @@ curl -N -X POST http://localhost:8000/chat/stream \
142
  -H 'Content-Type: application/json' \
143
  -d '{"user":"demo","session":"default","prompt":"Hello"}'
144
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  - ``POST /chat/stream`` – Stream the assistant's response as plain text.
136
  - ``POST /upload`` – Upload a document so it can be referenced in chats.
137
+ - ``GET /sessions/{user}`` – List available session names for ``user``.
138
 
139
  Example request:
140
 
 
143
  -H 'Content-Type: application/json' \
144
  -d '{"user":"demo","session":"default","prompt":"Hello"}'
145
  ```
146
+
147
+ ## CLI
148
+
149
+ An interactive command line interface is provided for Windows and other
150
+ platforms. Install the dependencies and run:
151
+
152
+ ```bash
153
+ python -m src.cli --user yourname
154
+ ```
155
+
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.
requirements.txt CHANGED
@@ -7,3 +7,5 @@ python-dotenv
7
  fastapi
8
  uvicorn
9
  python-multipart
 
 
 
7
  fastapi
8
  uvicorn
9
  python-multipart
10
+ httpx
11
+ typer
src/api.py CHANGED
@@ -10,6 +10,7 @@ from pathlib import Path
10
 
11
  from .chat import ChatSession
12
  from .log import get_logger
 
13
 
14
 
15
  _LOG = get_logger(__name__)
@@ -58,6 +59,10 @@ def create_app() -> FastAPI:
58
  pass
59
  return {"path": vm_path}
60
 
 
 
 
 
61
  @app.get("/health")
62
  async def health():
63
  return {"status": "ok"}
 
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__)
 
59
  pass
60
  return {"path": vm_path}
61
 
62
+ @app.get("/sessions/{user}")
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"}
src/cli.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import AsyncIterator
5
+
6
+ import httpx
7
+ import typer
8
+ from colorama import Fore, Style, init
9
+
10
+
11
+ API_URL = "http://localhost:8000"
12
+
13
+ app = typer.Typer(add_completion=False, help="Interact with the LLM backend API")
14
+
15
+
16
+ async def _get_sessions(user: str, server: str) -> list[str]:
17
+ async with httpx.AsyncClient(base_url=server) as client:
18
+ resp = await client.get(f"/sessions/{user}")
19
+ resp.raise_for_status()
20
+ data = resp.json()
21
+ return data.get("sessions", [])
22
+
23
+
24
+ async def _stream_chat(
25
+ user: str, session: str, prompt: str, server: str
26
+ ) -> AsyncIterator[str]:
27
+ async with httpx.AsyncClient(base_url=server, timeout=None) as client:
28
+ async with client.stream(
29
+ "POST",
30
+ "/chat/stream",
31
+ json={"user": user, "session": session, "prompt": prompt},
32
+ ) as resp:
33
+ resp.raise_for_status()
34
+ async for line in resp.aiter_lines():
35
+ if line:
36
+ yield line
37
+
38
+
39
+ async def _chat_loop(user: str, server: str) -> None:
40
+ init(autoreset=True)
41
+ sessions = await _get_sessions(user, server)
42
+ session = "default"
43
+ if sessions:
44
+ typer.echo("Existing sessions:")
45
+ for idx, name in enumerate(sessions, 1):
46
+ typer.echo(f" {idx}. {name}")
47
+ choice = typer.prompt(
48
+ "Select session number or enter new name", default=str(len(sessions))
49
+ )
50
+ if choice.isdigit() and 1 <= int(choice) <= len(sessions):
51
+ session = sessions[int(choice) - 1]
52
+ else:
53
+ session = choice.strip() or session
54
+ else:
55
+ session = typer.prompt("Session name", default=session)
56
+
57
+ typer.echo(
58
+ f"Chatting as {Fore.GREEN}{user}{Style.RESET_ALL} in session '{session}'"
59
+ )
60
+
61
+ while True:
62
+ try:
63
+ msg = typer.prompt(f"{Fore.CYAN}You{Style.RESET_ALL}")
64
+ except EOFError:
65
+ break
66
+ if msg.strip().lower() in {"exit", "quit"}:
67
+ break
68
+ async for part in _stream_chat(user, session, msg, server):
69
+ typer.echo(f"{Fore.YELLOW}{part}{Style.RESET_ALL}")
70
+
71
+
72
+ @app.callback(invoke_without_command=True)
73
+ def main(
74
+ user: str = typer.Option("default", "--user", "-u"),
75
+ server: str = typer.Option(API_URL, "--server", "-s"),
76
+ ) -> None:
77
+ """Start an interactive chat session."""
78
+
79
+ asyncio.run(_chat_loop(user, server))
80
+
81
+
82
+ if __name__ == "__main__": # pragma: no cover - manual execution
83
+ app()
src/db.py CHANGED
@@ -61,6 +61,7 @@ __all__ = [
61
  "Message",
62
  "Document",
63
  "reset_history",
 
64
  "add_document",
65
  ]
66
 
@@ -98,3 +99,14 @@ def add_document(username: str, file_path: str, original_name: str) -> Document:
98
  user, _ = User.get_or_create(username=username)
99
  doc = Document.create(user=user, file_path=file_path, original_name=original_name)
100
  return doc
 
 
 
 
 
 
 
 
 
 
 
 
61
  "Message",
62
  "Document",
63
  "reset_history",
64
+ "list_sessions",
65
  "add_document",
66
  ]
67
 
 
99
  user, _ = User.get_or_create(username=username)
100
  doc = Document.create(user=user, file_path=file_path, original_name=original_name)
101
  return doc
102
+
103
+
104
+ def list_sessions(username: str) -> list[str]:
105
+ """Return all session names for the given ``username``."""
106
+
107
+ init_db()
108
+ try:
109
+ user = User.get(User.username == username)
110
+ except User.DoesNotExist:
111
+ return []
112
+ return [c.session_name for c in Conversation.select().where(Conversation.user == user)]