Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# dependencies
node_modules/

*.keystore

# Expo
.expo/
dist/
Expand Down
13 changes: 13 additions & 0 deletions SupportServer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip tini \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
COPY . .

ENTRYPOINT ["/usr/bin/tini","--"]
CMD ["python3","-m","uvicorn","main:app","--host","0.0.0.0","--port","8000"]
145 changes: 145 additions & 0 deletions SupportServer/agent_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
## To start a single-agent CLI, run:
## python agent_cli.py --url ws://localhost:8000/ws ##
## python3 agent_cli.py --url wss://deviation-addressing-adjust-trying.trycloudflare.com/ws
## Replace the URL with your own WebSocket endpoint.


import asyncio
import json
import sys
import argparse ## For command-line argument parsing
from datetime import datetime
from datetime import timezone
import contextlib

import websockets


def ts_short():
""" For timestammping use"""
return datetime.now(timezone.utc).strftime("%H:%M:%S")


async def stdin_reader(queue: asyncio.Queue[str]):
## Get currently running loop, to schedule running tasks
loop = asyncio.get_running_loop()
while True:
## This will ensure blocking the readline doesnt block the main loop
line = await loop.run_in_executor(None, sys.stdin.readline)
if line == "":
await queue.put("/quit")
break
await queue.put(line.rstrip("\n"))


## Run the agent CLI with given URL
async def agent_cli(url: str):
## Append the role=agent if not present in the websocket provided
sep = "&" if "?" in url else "?"
if "role=" not in url:
url = f"{url}{sep}role=agent"

print(f"[cli] Connecting to {url} ...")

async with websockets.connect(url) as ws:
raw = await ws.recv()
try:
msg = json.loads(raw)
except Exception:
print("[cli] Unexpected non-JSON from server:", raw)
return
if msg.get("type") != "ready" or msg.get("role") != "agent":
print("[cli] Unexpected greeting:", msg)
return

print("[cli] Connected as AGENT. Waiting for assignment…")
current_conv: str | None = None

## stdin will push typed lines into here, FIFO
inbox_q: asyncio.Queue[str] = asyncio.Queue()
stdin_task = asyncio.create_task(stdin_reader(inbox_q))

## This has to run concurrently with the stdin loop
async def ws_recv_loop():
nonlocal current_conv
while True:
raw_in = await ws.recv()
try:
data = json.loads(raw_in)
except Exception:
print("[srv] (non-JSON)", raw_in)
continue

t = data.get("type")
## Handling assigned messages sent in do_assign() in main.py
if t == "assigned":
current_conv = data.get("conversationId")
partner = data.get("partner", {})
print(f"[{ts_short()}] Assigned conversation: {current_conv} (partner={partner.get('role')})")
print("Type your reply. Use /end to finish, /quit to exit.")
elif t == "message.new":
m = data.get("message", {})
role = m.get("sender_role")
content = m.get("content")
print(f"[{ts_short()}] {role}: {content}")
elif t == "ended":
reason = data.get("reason")
print(f"[{ts_short()}] Conversation ended (reason={reason}). Waiting for next assignment…")
current_conv = None
elif t == "error":
print(f"[srv:ERROR] {data}")
else:
pass

## Initialise the websocket receiving loop
ws_task = asyncio.create_task(ws_recv_loop())


## Inbox processing
try:
while True:
line = await inbox_q.get()
if line.strip() == "":
continue
if line.strip().lower() == "/quit":
print("[cli] Quitting…")
break
if line.strip().lower() == "/end":
if current_conv is None:
print("[cli] No active conversation.")
continue
await ws.send(json.dumps({
"type": "end",
"conversationId": current_conv,
}))
continue

if current_conv is None:
print("[cli] Not assigned yet. Your message will be ignored. Use /quit to exit.")
continue

payload = {
"type": "message.send",
"conversationId": current_conv,
"content": line,
}
await ws.send(json.dumps(payload))
except websockets.ConnectionClosed as e:
print(f"[cli] Connection closed: {e}")
finally:
ws_task.cancel()
stdin_task.cancel()
with contextlib.suppress(Exception):
await ws.close()


if __name__ == "__main__":

parser = argparse.ArgumentParser(description="Single-agent chat CLI")
parser.add_argument("--url", default="ws://localhost:8000/ws", help="WebSocket endpoint")
args = parser.parse_args()

try:
asyncio.run(agent_cli(args.url))
except KeyboardInterrupt:
print("[cli] Interrupted.")
26 changes: 26 additions & 0 deletions SupportServer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
services:
app:
build: .
image: chat-app:latest # tag the image
container_name: chat-app
ports:
- "8000:8000"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "python3 - <<'PY'\nimport urllib.request,sys\ntry:\n urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)\n sys.exit(0)\nexcept Exception:\n sys.exit(1)\nPY"]
interval: 10s
timeout: 5s
retries: 5

agent:
image: chat-app:latest # reuse same image
container_name: chat-agent
depends_on:
app:
condition: service_healthy
# INTERACTIVE TTY:
stdin_open: true # keeps STDIN open
tty: true # allocates a TTY
restart: "no" # don't auto-restart when you exit the agent
working_dir: /app
command: ["python3","agent_cli.py","--url","ws://app:8000/ws"]
Loading
Loading