-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchat.py
More file actions
103 lines (82 loc) · 2.79 KB
/
chat.py
File metadata and controls
103 lines (82 loc) · 2.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
from __future__ import annotations
import json
import time
import uuid
from typing import Any
from flask import Blueprint, Response, jsonify, request, stream_with_context, session
from app.ai_calls import call_ai
bp = Blueprint("chat", __name__, url_prefix="/api")
@bp.get("/session")
def browser_session():
"""Stable per-browser id (signed cookie session). Used for server-side scoping."""
return jsonify({"browserSessionId": session.get("browser_id")})
def _text_from_parts(parts: list[Any] | None) -> str:
if not parts:
return ""
out: list[str] = []
for p in parts:
if isinstance(p, dict) and p.get("type") == "text":
out.append(str(p.get("text", "")))
return "".join(out).strip()
def _ndjson_line(obj: dict) -> str:
return json.dumps(obj, separators=(",", ":")) + "\n"
@bp.post("/chat")
def chat_stream():
payload = request.get_json(silent=True) or {}
message = payload.get("message") or {}
user_message = _text_from_parts(message.get("parts"))
# Wire schema uses camelCase; backend uses snake_case internally.
editor_content = payload.get("editorContent") or ""
displayed_url = payload.get("displayedUrl") or payload.get("displayedURL") or ""
reply = call_ai(
{
"user_message": user_message,
"editor_content": editor_content,
"displayed_url": displayed_url,
},
session,
)
response = reply.response
editor_content = reply.editor_content
documentation_url = reply.documentation_url
def generate():
message_id = str(uuid.uuid4())
text_id = "assistant-text-1"
yield _ndjson_line(
{
"type": "start",
"messageId": message_id,
}
)
# Send UI state separately from text deltas to keep the streaming protocol lean.
yield _ndjson_line(
{
"type": "ui-state",
"editorContent": editor_content,
"displayedUrl": documentation_url,
}
)
step = 6
for i in range(0, len(response), step):
chunk = response[i: i + step]
yield _ndjson_line(
{
"type": "text-delta",
"id": text_id,
"delta": chunk,
}
)
time.sleep(0.04)
yield _ndjson_line({"type": "text-end", "id": text_id})
yield _ndjson_line(
{
"type": "finish",
"messageId": message_id,
"editorContent": editor_content,
"displayedUrl": documentation_url,
}
)
return Response(
stream_with_context(generate()),
mimetype="application/x-ndjson; charset=utf-8",
)