create-workframe 0.1.14 → 0.1.15
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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/sync-canonical-to-package.mjs +3 -0
- package/workframe-api/activity_feed.py +53 -5
- package/workframe-api/api_errors.py +126 -0
- package/workframe-api/chat_bind.py +8 -1
- package/workframe-api/chat_stream.py +70 -26
- package/workframe-api/crew_registry.py +3 -6
- package/workframe-api/google_auth.py +20 -12
- package/workframe-api/handler_modules/handler_auth.py +66 -21
- package/workframe-api/handler_modules/handler_chat.py +25 -0
- package/workframe-api/handler_modules/handler_install.py +45 -0
- package/workframe-api/handler_modules/handler_provider.py +1 -0
- package/workframe-api/handler_modules/handler_workspace.py +30 -16
- package/workframe-api/hermes_admin.py +9 -2
- package/workframe-api/hermes_profiles.py +25 -5
- package/workframe-api/install_api.py +38 -4
- package/workframe-api/lane_bindings.py +24 -0
- package/workframe-api/llm_error_glossary.py +58 -4
- package/workframe-api/mention_invoke.py +89 -27
- package/workframe-api/model_surface.py +1 -1
- package/workframe-api/provider_bindings.py +44 -2
- package/workframe-api/provider_bootstrap.py +9 -0
- package/workframe-api/provider_catalog.py +3 -3
- package/workframe-api/rooms.py +3 -0
- package/workframe-api/route_registry.py +1 -0
- package/workframe-api/run_ledger.py +52 -8
- package/workframe-api/runtime_cohort.py +11 -1
- package/workframe-api/server.py +25 -1
- package/workframe-api/stack_config.py +4 -1
- package/workframe-api/test_api_errors.py +33 -0
- package/workframe-api/test_enrich_room_messages.py +72 -0
- package/workframe-api/test_exception_hygiene.py +4 -2
- package/workframe-api/test_hermes_admin.py +7 -0
- package/workframe-api/test_install_wizard_resume.py +39 -13
- package/workframe-api/test_model_surface_consistency.py +118 -100
- package/workframe-api/test_profile_model_yaml.py +4 -1
- package/workframe-api/test_provider_bindings.py +10 -0
- package/workframe-api/test_run_ledger.py +2 -1
- package/workframe-api/test_server_reexports.py +4 -0
- package/workframe-api/test_user_llm_prefs.py +99 -0
- package/workframe-api/time-overlay-split.py +58 -0
- package/workframe-api/time-ttft-quick.py +135 -0
- package/workframe-api/time-ttft.py +250 -0
- package/workframe-api/time-wf-stream.py +145 -0
- package/workframe-api/turn_overlay.py +96 -23
- package/workframe-api/updates.py +1 -2
- package/workframe-api/user_prefs.py +26 -5
- package/workframe-api/workspace_bootstrap.py +16 -10
- package/workframe-ui/public/assets/{arc-BT9iZUSd.js → arc-BARqpbNX.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CYE89P6J.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-CaEDd3np.js → architectureDiagram-3BPJPVTR-bbpW6hfw.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-DHJhMrIS.js → blockDiagram-GPEHLZMM-aG0oR9eA.js} +1 -1
- package/workframe-ui/public/assets/brave-B885JYnH.svg +11 -0
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Cdyz9hIV.js → c4Diagram-AAUBKEIU-BDi4E5jK.js} +1 -1
- package/workframe-ui/public/assets/channel-B4ex0Z2H.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-BbZx76LX.js → chunk-2J33WTMH-BBU5tRNe.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DcSxjTIP.js → chunk-3OPIFGDE-SKhe5YD8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-B7F-kTY1.js → chunk-4BX2VUAB-D4uKMCPc.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-BwwEiRTK.js → chunk-55IACEB6-SnqJA0wG.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DAi_NksX.js → chunk-5ZQYHXKU-ZyBWNsT2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BIaF4XCN.js → chunk-727SXJPM-DbxwKsiK.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DjYSk1dG.js → chunk-AQP2D5EJ-Bm2iWcL0.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-_twnyfgV.js → chunk-BSJP7CBP-CGwyQh1d.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CkNmdy5s.js → chunk-CSCIHK7Q-XWSO96VW.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CjLqd-gl.js → chunk-FMBD7UC4-D0OTpUqL.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-_6oNqFjj.js → chunk-KSCS5N6A-CM75wJ6j.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CeO9Am_D.js → chunk-L5ZTLDWV-BCgtwaGP.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-sRM4YeIe.js → chunk-LZXEDZCA-CZBc86Td.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-CH8tMb21.js → chunk-ND2GUHAM-DIq9-Qtg.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-DAI1bsII.js → chunk-NZK2D7GU-BTOOFYOR.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-B2e-5a2G.js → chunk-O5CBEL6O-CYriwTot.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-sPK7umZx.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DtLDSmiK.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-DM2-0q37.js → chunk-XPW4576I-6iEhdiBi.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DWLy8pDS.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DWLy8pDS.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-BnbbewLv.js → cose-bilkent-S5V4N54A-C716vjLB.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-C4zmB1Vt.js → dagre-BM42HDAG-Cs-MNVkM.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-n-BJmsiC.js → diagram-2AECGRRQ-BqoyTWyW.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzQGQnZi.js → diagram-5GNKFQAL-CDRTV5fY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Cx_De-wS.js → diagram-KO2AKTUF-u9NjixEi.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CIOI3Lok.js → diagram-LMA3HP47-rDd6T2os.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-DkRuPLWY.js → diagram-OG6HWLK6-Bl4VX9RO.js} +1 -1
- package/workframe-ui/public/assets/{dist-XhCT1Q-V.js → dist-DSCcbFgV.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-C3GwhDU1.js → erDiagram-TEJ5UH35-C9Nv-5nl.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-B7kChV1b.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CLWH8z2e.js → flowDiagram-I6XJVG4X-D8Xhbd7s.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-D4h66Vy3.js → ganttDiagram-6RSMTGT7-CMycBZqm.js} +1 -1
- package/workframe-ui/public/assets/gemini-Cq-0-rmr.svg +105 -0
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-IW5F-Q6c.js → gitGraph-WXDBUCRP-DnIro8tZ.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BjNIbmRM.js → gitGraphDiagram-PVQCEYII-B74H9ISZ.js} +1 -1
- package/workframe-ui/public/assets/index-BLVAsAeO.css +1 -0
- package/workframe-ui/public/assets/index-s4167RRh.js +132 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-CcJspM79.js → info-J43DQDTF-B6mVw1B6.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-DP4xR9jt.js → infoDiagram-5YYISTIA-Ddtfq_xy.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-D4cZb21V.js → ishikawaDiagram-YF4QCWOH-DBb6veAL.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DsLuHkIJ.js → journeyDiagram-JHISSGLW-DMgdeS5h.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Wu65nOsR.js → kanban-definition-UN3LZRKU-DiV_LoDN.js} +1 -1
- package/workframe-ui/public/assets/{line-ByBDzRpy.js → line-BR9uEes_.js} +1 -1
- package/workframe-ui/public/assets/{linear-DzXi2vnq.js → linear-BhjkJfy9.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-CaB-1Ckn.js → mermaid-parser.core-CEWtaJIq.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-D7HTHC17.js → mermaid.core-B6xKLKp-.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BfKsC9aj.js → mindmap-definition-RKZ34NQL-DypZm9OU.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-ClpU98TO.js → packet-YPE3B663-CCxb0WDS.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-KeOyY_-u.js → pie-LRSECV5Y-pTkr4g8B.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CUpjnzbs.js → pieDiagram-4H26LBE5-BventgTb.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CJdPTszF.js → quadrantDiagram-W4KKPZXB-BQnvFJYk.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-C9BpandZ.js → radar-GUYGQ44K-EX9Pzmli.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-CHDZPwry.js → requirementDiagram-4Y6WPE33-Bnw4xecM.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-CMJJh91s.js → sankeyDiagram-5OEKKPKP-spHh-EYp.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BsV4GppP.js → sequenceDiagram-3UESZ5HK-CC97Qcbf.js} +1 -1
- package/workframe-ui/public/assets/{src-WHks82Up.js → src-DRcBNyeZ.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-eSX9P8z0.js → stateDiagram-AJRCARHV-C8r8rx81.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CnedwxVT.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-Dg1UkhGo.js → timeline-definition-PNZ67QCA-CA4JxR_w.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-B7yGsBuj.js → treeView-BLDUP644-BuGSo6xP.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-j_G2oyQ_.js → treemap-LRROVOQU-JyJMArE9.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BRrT0jqC.js → vennDiagram-CIIHVFJN-BZAUo7xL.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-DEdUqez2.js → wardley-L42UT6IY-CMgm8t9M.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-C-bBK9Bb.js → wardleyDiagram-YWT4CUSO-CqI1c2zN.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DhH5xuR2.js → xychartDiagram-2RQKCTM6-DhTdH-jJ.js} +1 -1
- package/workframe-ui/public/index.html +2 -2
- package/workframe-ui/public/workframe-build.json +3 -3
- package/docs/security.md +0 -85
- package/workframe-ui/public/assets/architecture-7EHR7CIX-m1aPUQ_r.js +0 -1
- package/workframe-ui/public/assets/channel-3M69ekE5.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-BMl9ed8P.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-C08HH6BU.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BFcOE4Aq.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BFcOE4Aq.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-DIVBL6CF.js +0 -1
- package/workframe-ui/public/assets/index-DL2vJP4L.css +0 -1
- package/workframe-ui/public/assets/index-ElS_D59P.js +0 -130
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-FzMK4PkM.js +0 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Quick TTFT probe: pre-stream phases + upstream first byte/delta (90s cap)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
import time
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
|
|
12
|
+
import server
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ms(t0: float) -> int:
|
|
16
|
+
return round((time.perf_counter() - t0) * 1000)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
conn = sqlite3.connect(str(server.DATA_DIR / "workframe.db"))
|
|
21
|
+
conn.row_factory = sqlite3.Row
|
|
22
|
+
room = conn.execute(
|
|
23
|
+
"""
|
|
24
|
+
SELECT r.id, r.workspace_id, ap.slug AS agent_slug
|
|
25
|
+
FROM rooms r
|
|
26
|
+
JOIN agent_profiles ap ON ap.id = r.agent_profile_id
|
|
27
|
+
JOIN room_memberships rm ON rm.room_id = r.id AND rm.status = 'active'
|
|
28
|
+
WHERE r.room_type = 'direct' AND r.deleted_at IS NULL
|
|
29
|
+
ORDER BY r.updated_at DESC
|
|
30
|
+
LIMIT 1
|
|
31
|
+
""",
|
|
32
|
+
).fetchone()
|
|
33
|
+
if not room:
|
|
34
|
+
raise SystemExit("no direct room")
|
|
35
|
+
user = conn.execute(
|
|
36
|
+
"""
|
|
37
|
+
SELECT u.id FROM users u
|
|
38
|
+
JOIN room_memberships rm ON rm.user_id = u.id AND rm.room_id = ?
|
|
39
|
+
WHERE u.status = 'active'
|
|
40
|
+
LIMIT 1
|
|
41
|
+
""",
|
|
42
|
+
(str(room["id"]),),
|
|
43
|
+
).fetchone()
|
|
44
|
+
conn.close()
|
|
45
|
+
if not user:
|
|
46
|
+
raise SystemExit("no room user")
|
|
47
|
+
|
|
48
|
+
room_id = str(room["id"])
|
|
49
|
+
ws_id = str(room["workspace_id"])
|
|
50
|
+
user_id = str(user["id"])
|
|
51
|
+
agent_slug = str(room["agent_slug"])
|
|
52
|
+
|
|
53
|
+
t = time.perf_counter()
|
|
54
|
+
prof = server._resolve_chat_hermes_profile(agent_slug, user_id, room_id, ws_id)
|
|
55
|
+
print(f"resolve_ms={ms(t)} profile={prof}")
|
|
56
|
+
|
|
57
|
+
payload = {
|
|
58
|
+
"room_id": room_id,
|
|
59
|
+
"workspace_id": ws_id,
|
|
60
|
+
"source_id": "ttft-quick",
|
|
61
|
+
"client_id": "ttft-quick",
|
|
62
|
+
"binding_version": 2,
|
|
63
|
+
}
|
|
64
|
+
t = time.perf_counter()
|
|
65
|
+
session = server.profile_chat_session(agent_slug, payload, user_id=user_id)
|
|
66
|
+
sid = str(session.get("session_id") or "")
|
|
67
|
+
print(f"bind_ms={ms(t)} session={sid[:36]}")
|
|
68
|
+
|
|
69
|
+
provider = server._llm_billing_provider(prof, user_id=user_id, workspace_id=ws_id)
|
|
70
|
+
model = str(server._read_model_block(prof).get("default") or "")
|
|
71
|
+
print(f"provider={provider} model={model}")
|
|
72
|
+
|
|
73
|
+
for i in range(3):
|
|
74
|
+
t = time.perf_counter()
|
|
75
|
+
server._overlay_turn_provider_env(prof, user_id, ws_id, provider, f"quick-{i}")
|
|
76
|
+
server._overlay_turn_user_env(prof, user_id, ws_id, f"quick-{i}")
|
|
77
|
+
print(f"overlay_turn_{i}_ms={ms(t)}")
|
|
78
|
+
|
|
79
|
+
port = server._profile_api_port(prof)
|
|
80
|
+
key = server._profile_api_key(prof)
|
|
81
|
+
body = json.dumps(server._profile_turn_payload(prof, "Reply with exactly: pong", room_id)).encode()
|
|
82
|
+
url = f"http://gateway:{port}/api/sessions/{urllib.parse.quote(sid, safe='')}/chat/stream"
|
|
83
|
+
req = urllib.request.Request(
|
|
84
|
+
url,
|
|
85
|
+
data=body,
|
|
86
|
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
|
87
|
+
method="POST",
|
|
88
|
+
)
|
|
89
|
+
t0 = time.perf_counter()
|
|
90
|
+
first_byte = first_delta = first_error = None
|
|
91
|
+
try:
|
|
92
|
+
with urllib.request.urlopen(req, timeout=90) as resp:
|
|
93
|
+
buf = b""
|
|
94
|
+
while True:
|
|
95
|
+
chunk = resp.read(4096)
|
|
96
|
+
if not chunk:
|
|
97
|
+
break
|
|
98
|
+
if first_byte is None:
|
|
99
|
+
first_byte = ms(t0)
|
|
100
|
+
buf += chunk
|
|
101
|
+
while b"\n\n" in buf:
|
|
102
|
+
frame, buf = buf.split(b"\n\n", 1)
|
|
103
|
+
text = frame.decode("utf-8", errors="replace")
|
|
104
|
+
ev = ""
|
|
105
|
+
data = ""
|
|
106
|
+
for line in text.splitlines():
|
|
107
|
+
if line.startswith("event:"):
|
|
108
|
+
ev = line[6:].strip()
|
|
109
|
+
elif line.startswith("data:"):
|
|
110
|
+
data = line[5:].lstrip()
|
|
111
|
+
if ev in ("message.delta", "assistant.delta") and first_delta is None:
|
|
112
|
+
first_delta = ms(t0)
|
|
113
|
+
print(f"upstream_first_delta_ms={first_delta}")
|
|
114
|
+
return 0
|
|
115
|
+
if ev in ("error", "run.failed", "concierge") and first_error is None:
|
|
116
|
+
first_error = ms(t0)
|
|
117
|
+
print(f"upstream_first_error_ms={first_error} data={data[:240]}")
|
|
118
|
+
return 0
|
|
119
|
+
except urllib.error.HTTPError as exc:
|
|
120
|
+
print(f"upstream_http_error_ms={ms(t0)} code={exc.code} body={exc.read()[:240]!r}")
|
|
121
|
+
return 1
|
|
122
|
+
except OSError as exc:
|
|
123
|
+
print(f"upstream_os_error_ms={ms(t0)} err={exc}")
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
print(
|
|
127
|
+
f"upstream_first_byte_ms={first_byte} "
|
|
128
|
+
f"first_delta_ms={first_delta} first_error_ms={first_error} "
|
|
129
|
+
f"total_ms={ms(t0)}"
|
|
130
|
+
)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Measure time-to-first-token through the Workframe stream path (docker exec)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import sqlite3
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from http.server import BaseHTTPRequestHandler
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import server
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ms(t0: float) -> int:
|
|
20
|
+
return round((time.perf_counter() - t0) * 1000)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _CaptureHandler(BaseHTTPRequestHandler):
|
|
24
|
+
protocol_version = "HTTP/1.1"
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self.headers_sent = False
|
|
28
|
+
self.events: list[tuple[float, str, str]] = []
|
|
29
|
+
self._t0 = time.perf_counter()
|
|
30
|
+
self.rfile = io.BytesIO()
|
|
31
|
+
self.wfile = _WriteCapture(self)
|
|
32
|
+
|
|
33
|
+
def log_message(self, format: str, *args: Any) -> None: # noqa: A003
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
def send_response(self, code: int, message: str | None = None) -> None:
|
|
37
|
+
self.status_code = code
|
|
38
|
+
|
|
39
|
+
def send_header(self, keyword: str, value: str) -> None:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def end_headers(self) -> None:
|
|
43
|
+
self.headers_sent = True
|
|
44
|
+
self.events.append((time.perf_counter(), "headers", ""))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _WriteCapture(io.BufferedIOBase):
|
|
48
|
+
def __init__(self, handler: _CaptureHandler) -> None:
|
|
49
|
+
self._handler = handler
|
|
50
|
+
self._buf = bytearray()
|
|
51
|
+
|
|
52
|
+
def writable(self) -> bool:
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
def write(self, b: bytes) -> int:
|
|
56
|
+
self._buf.extend(b)
|
|
57
|
+
while b"\n\n" in self._buf:
|
|
58
|
+
frame, self._buf = self._buf.split(b"\n\n", 1)
|
|
59
|
+
self._record_frame(frame + b"\n\n")
|
|
60
|
+
return len(b)
|
|
61
|
+
|
|
62
|
+
def flush(self) -> None:
|
|
63
|
+
if self._buf:
|
|
64
|
+
self._record_frame(bytes(self._buf))
|
|
65
|
+
self._buf.clear()
|
|
66
|
+
|
|
67
|
+
def _record_frame(self, frame: bytes) -> None:
|
|
68
|
+
text = frame.decode("utf-8", errors="replace")
|
|
69
|
+
event = ""
|
|
70
|
+
data = ""
|
|
71
|
+
for line in text.splitlines():
|
|
72
|
+
if line.startswith("event:"):
|
|
73
|
+
event = line[6:].strip()
|
|
74
|
+
elif line.startswith("data:"):
|
|
75
|
+
data = line[5:].lstrip()
|
|
76
|
+
self._handler.events.append((time.perf_counter(), event, data))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _find_room_user() -> tuple[dict[str, Any], str]:
|
|
80
|
+
conn = sqlite3.connect(str(server.DATA_DIR / "workframe.db"))
|
|
81
|
+
conn.row_factory = sqlite3.Row
|
|
82
|
+
room = conn.execute(
|
|
83
|
+
"""
|
|
84
|
+
SELECT r.id, r.workspace_id, ap.slug AS agent_slug
|
|
85
|
+
FROM rooms r
|
|
86
|
+
JOIN agent_profiles ap ON ap.id = r.agent_profile_id
|
|
87
|
+
JOIN room_memberships rm ON rm.room_id = r.id AND rm.status = 'active'
|
|
88
|
+
WHERE r.room_type = 'direct' AND r.deleted_at IS NULL
|
|
89
|
+
ORDER BY r.updated_at DESC
|
|
90
|
+
LIMIT 1
|
|
91
|
+
""",
|
|
92
|
+
).fetchone()
|
|
93
|
+
if not room:
|
|
94
|
+
conn.close()
|
|
95
|
+
raise SystemExit("no direct room")
|
|
96
|
+
user = conn.execute(
|
|
97
|
+
"""
|
|
98
|
+
SELECT u.id FROM users u
|
|
99
|
+
JOIN room_memberships rm ON rm.user_id = u.id AND rm.room_id = ?
|
|
100
|
+
WHERE u.status = 'active'
|
|
101
|
+
LIMIT 1
|
|
102
|
+
""",
|
|
103
|
+
(str(room["id"]),),
|
|
104
|
+
).fetchone()
|
|
105
|
+
conn.close()
|
|
106
|
+
if not user:
|
|
107
|
+
raise SystemExit("no room user")
|
|
108
|
+
return dict(room), str(user["id"])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _report_handler_events(handler: _CaptureHandler, t0: float, label: str) -> int | None:
|
|
112
|
+
first_delta: int | None = None
|
|
113
|
+
for at, event, _data in handler.events:
|
|
114
|
+
rel = ms(t0)
|
|
115
|
+
if event in ("run.started", "message.delta", "error", "concierge", "message.complete", "done"):
|
|
116
|
+
print(f" {label}_{event}_ms={rel}")
|
|
117
|
+
if event == "message.delta" and first_delta is None:
|
|
118
|
+
first_delta = rel
|
|
119
|
+
return first_delta
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _stream_upstream(prof: str, sid: str, text: str, room_id: str) -> dict[str, int | None]:
|
|
123
|
+
port = server._profile_api_port(prof)
|
|
124
|
+
key = server._profile_api_key(prof)
|
|
125
|
+
body = json.dumps(server._profile_turn_payload(prof, text, room_id)).encode()
|
|
126
|
+
url = f"http://gateway:{port}/api/sessions/{urllib.parse.quote(sid, safe='')}/chat/stream"
|
|
127
|
+
req = urllib.request.Request(
|
|
128
|
+
url,
|
|
129
|
+
data=body,
|
|
130
|
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
|
131
|
+
method="POST",
|
|
132
|
+
)
|
|
133
|
+
t0 = time.perf_counter()
|
|
134
|
+
out: dict[str, int | None] = {
|
|
135
|
+
"first_byte_ms": None,
|
|
136
|
+
"first_delta_ms": None,
|
|
137
|
+
"run_started_ms": None,
|
|
138
|
+
}
|
|
139
|
+
try:
|
|
140
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
141
|
+
buf = b""
|
|
142
|
+
while True:
|
|
143
|
+
chunk = resp.read(4096)
|
|
144
|
+
if not chunk:
|
|
145
|
+
break
|
|
146
|
+
if out["first_byte_ms"] is None:
|
|
147
|
+
out["first_byte_ms"] = ms(t0)
|
|
148
|
+
buf += chunk
|
|
149
|
+
while b"\n\n" in buf:
|
|
150
|
+
frame, buf = buf.split(b"\n\n", 1)
|
|
151
|
+
event = ""
|
|
152
|
+
for line in frame.decode("utf-8", errors="replace").splitlines():
|
|
153
|
+
if line.startswith("event:"):
|
|
154
|
+
event = line[6:].strip()
|
|
155
|
+
if event == "run.started" and out["run_started_ms"] is None:
|
|
156
|
+
out["run_started_ms"] = ms(t0)
|
|
157
|
+
if event in ("message.delta", "assistant.delta") and out["first_delta_ms"] is None:
|
|
158
|
+
out["first_delta_ms"] = ms(t0)
|
|
159
|
+
return out
|
|
160
|
+
except urllib.error.HTTPError as exc:
|
|
161
|
+
print(f"upstream_http_error={exc.code} body={exc.read()[:240]!r}")
|
|
162
|
+
except OSError as exc:
|
|
163
|
+
print(f"upstream_error={exc}")
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main() -> int:
|
|
168
|
+
import chat_stream
|
|
169
|
+
|
|
170
|
+
room, user_id = _find_room_user()
|
|
171
|
+
room_id = str(room["id"])
|
|
172
|
+
workspace_id = str(room["workspace_id"])
|
|
173
|
+
agent_slug = str(room["agent_slug"])
|
|
174
|
+
payload_bind = {
|
|
175
|
+
"room_id": room_id,
|
|
176
|
+
"workspace_id": workspace_id,
|
|
177
|
+
"source_id": "ttft",
|
|
178
|
+
"client_id": "ttft",
|
|
179
|
+
"binding_version": 2,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
t_resolve = time.perf_counter()
|
|
183
|
+
prof = server._resolve_chat_hermes_profile(agent_slug, user_id, room_id, workspace_id)
|
|
184
|
+
print(f"agent_slug={agent_slug} profile={prof} resolve_ms={ms(t_resolve)}")
|
|
185
|
+
|
|
186
|
+
healthy_before = server._profile_api_healthy(prof)
|
|
187
|
+
print(f"profile_api_healthy_before={healthy_before}")
|
|
188
|
+
|
|
189
|
+
t_bind = time.perf_counter()
|
|
190
|
+
session = server.profile_chat_session(agent_slug, payload_bind, user_id=user_id)
|
|
191
|
+
print(f"bind_ms={ms(t_bind)} session={str(session.get('session_id') or '')[:36]}")
|
|
192
|
+
|
|
193
|
+
provider = server._llm_billing_provider(prof, user_id=user_id, workspace_id=workspace_id)
|
|
194
|
+
model_block = server._read_model_block(prof)
|
|
195
|
+
model = str(model_block.get("default") or "")
|
|
196
|
+
|
|
197
|
+
# Phase timings (same work as stream_profile_chat pre-upstream)
|
|
198
|
+
t_auth = time.perf_counter()
|
|
199
|
+
import run_authority
|
|
200
|
+
import run_ledger
|
|
201
|
+
|
|
202
|
+
run_ledger.ensure_schema()
|
|
203
|
+
auth_req = run_authority.chat_run_request(
|
|
204
|
+
triggering_user_id=user_id,
|
|
205
|
+
profile_slug=prof,
|
|
206
|
+
workspace_id=workspace_id,
|
|
207
|
+
provider=provider,
|
|
208
|
+
room_id=room_id,
|
|
209
|
+
)
|
|
210
|
+
auth_ctx = chat_stream._run_authority_context_for_chat(user_id, workspace_id, provider)
|
|
211
|
+
run_authority.evaluate_run_authority(auth_req, auth_ctx, run_id="ttft-probe")
|
|
212
|
+
print(f"authority_ms={ms(t_auth)} provider={provider} model={model}")
|
|
213
|
+
|
|
214
|
+
t_api = time.perf_counter()
|
|
215
|
+
server.ensure_profile_api(prof, user_id, workspace_id)
|
|
216
|
+
ensure_ms = ms(t_api)
|
|
217
|
+
print(f"ensure_profile_api_ms={ensure_ms} healthy_after={server._profile_api_healthy(prof)}")
|
|
218
|
+
|
|
219
|
+
t_overlay = time.perf_counter()
|
|
220
|
+
server._overlay_turn_provider_env(prof, user_id, workspace_id, provider, "ttft-overlay")
|
|
221
|
+
server._overlay_turn_user_env(prof, user_id, workspace_id, "ttft-overlay")
|
|
222
|
+
print(f"overlay_ms={ms(t_overlay)}")
|
|
223
|
+
|
|
224
|
+
sid = str(session.get("session_id") or "")
|
|
225
|
+
stream_body = {
|
|
226
|
+
"session_id": sid,
|
|
227
|
+
"text": "Reply with exactly: pong",
|
|
228
|
+
"user_id": user_id,
|
|
229
|
+
"workspace_id": workspace_id,
|
|
230
|
+
"room_id": room_id,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Full Workframe stream path
|
|
234
|
+
handler = _CaptureHandler()
|
|
235
|
+
t_stream = time.perf_counter()
|
|
236
|
+
chat_stream.stream_profile_chat(handler, agent_slug, stream_body)
|
|
237
|
+
wf_ttft = _report_handler_events(handler, t_stream, "wf")
|
|
238
|
+
print(f"wf_ttft_ms={wf_ttft}")
|
|
239
|
+
|
|
240
|
+
# Direct upstream (gateway only) for comparison
|
|
241
|
+
upstream = _stream_upstream(prof, sid, "Reply with exactly: pong", room_id)
|
|
242
|
+
print(
|
|
243
|
+
"upstream "
|
|
244
|
+
+ " ".join(f"{k}={v}" for k, v in upstream.items() if v is not None)
|
|
245
|
+
)
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""WF stream path TTFT with phase breakdown (90s upstream cap)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import sqlite3
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from http.server import BaseHTTPRequestHandler
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import chat_stream
|
|
14
|
+
import server
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ms(t0: float) -> int:
|
|
18
|
+
return round((time.perf_counter() - t0) * 1000)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _CaptureHandler(BaseHTTPRequestHandler):
|
|
22
|
+
protocol_version = "HTTP/1.1"
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self.events: list[tuple[float, str]] = []
|
|
26
|
+
self.rfile = io.BytesIO()
|
|
27
|
+
self.wfile = _WriteCapture(self)
|
|
28
|
+
|
|
29
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
def send_response(self, code: int, message: str | None = None) -> None:
|
|
33
|
+
self.status_code = code
|
|
34
|
+
|
|
35
|
+
def send_header(self, keyword: str, value: str) -> None:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def end_headers(self) -> None:
|
|
39
|
+
self.events.append((time.perf_counter(), "headers"))
|
|
40
|
+
|
|
41
|
+
def flush(self) -> None:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _WriteCapture(io.BufferedIOBase):
|
|
46
|
+
def __init__(self, handler: _CaptureHandler) -> None:
|
|
47
|
+
self._handler = handler
|
|
48
|
+
self._buf = bytearray()
|
|
49
|
+
|
|
50
|
+
def writable(self) -> bool:
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
def write(self, b: bytes) -> int:
|
|
54
|
+
self._buf.extend(b)
|
|
55
|
+
while b"\n\n" in self._buf:
|
|
56
|
+
frame, self._buf = self._buf.split(b"\n\n", 1)
|
|
57
|
+
self._record_frame(frame + b"\n\n")
|
|
58
|
+
return len(b)
|
|
59
|
+
|
|
60
|
+
def flush(self) -> None:
|
|
61
|
+
if self._buf:
|
|
62
|
+
self._record_frame(bytes(self._buf))
|
|
63
|
+
self._buf.clear()
|
|
64
|
+
|
|
65
|
+
def _record_frame(self, frame: bytes) -> None:
|
|
66
|
+
text = frame.decode("utf-8", errors="replace")
|
|
67
|
+
event = ""
|
|
68
|
+
for line in text.splitlines():
|
|
69
|
+
if line.startswith("event:"):
|
|
70
|
+
event = line[6:].strip()
|
|
71
|
+
if event:
|
|
72
|
+
self._handler.events.append((time.perf_counter(), event))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main() -> int:
|
|
76
|
+
conn = sqlite3.connect(str(server.DATA_DIR / "workframe.db"))
|
|
77
|
+
conn.row_factory = sqlite3.Row
|
|
78
|
+
room = conn.execute(
|
|
79
|
+
"""
|
|
80
|
+
SELECT r.id, r.workspace_id, ap.slug AS agent_slug
|
|
81
|
+
FROM rooms r
|
|
82
|
+
JOIN agent_profiles ap ON ap.id = r.agent_profile_id
|
|
83
|
+
JOIN room_memberships rm ON rm.room_id = r.id AND rm.status = 'active'
|
|
84
|
+
WHERE r.room_type = 'direct' AND r.deleted_at IS NULL
|
|
85
|
+
ORDER BY r.updated_at DESC
|
|
86
|
+
LIMIT 1
|
|
87
|
+
""",
|
|
88
|
+
).fetchone()
|
|
89
|
+
user = conn.execute(
|
|
90
|
+
"""
|
|
91
|
+
SELECT u.id FROM users u
|
|
92
|
+
JOIN room_memberships rm ON rm.user_id = u.id AND rm.room_id = ?
|
|
93
|
+
WHERE u.status = 'active'
|
|
94
|
+
LIMIT 1
|
|
95
|
+
""",
|
|
96
|
+
(str(room["id"]),),
|
|
97
|
+
).fetchone()
|
|
98
|
+
conn.close()
|
|
99
|
+
if not room or not user:
|
|
100
|
+
print("no room/user", file=sys.stderr)
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
room_id = str(room["id"])
|
|
104
|
+
ws_id = str(room["workspace_id"])
|
|
105
|
+
user_id = str(user["id"])
|
|
106
|
+
agent_slug = str(room["agent_slug"])
|
|
107
|
+
|
|
108
|
+
payload_bind = {
|
|
109
|
+
"room_id": room_id,
|
|
110
|
+
"workspace_id": ws_id,
|
|
111
|
+
"source_id": "wf-ttft",
|
|
112
|
+
"client_id": "wf-ttft",
|
|
113
|
+
"binding_version": 2,
|
|
114
|
+
}
|
|
115
|
+
session = server.profile_chat_session(agent_slug, payload_bind, user_id=user_id)
|
|
116
|
+
sid = str(session.get("session_id") or "")
|
|
117
|
+
|
|
118
|
+
handler = _CaptureHandler()
|
|
119
|
+
stream_body = {
|
|
120
|
+
"session_id": sid,
|
|
121
|
+
"text": "Reply with exactly: pong",
|
|
122
|
+
"user_id": user_id,
|
|
123
|
+
"workspace_id": ws_id,
|
|
124
|
+
"room_id": room_id,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
t0 = time.perf_counter()
|
|
128
|
+
try:
|
|
129
|
+
chat_stream.stream_profile_chat(handler, agent_slug, stream_body)
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
print(f"stream_error_ms={ms(t0)} err={exc}")
|
|
132
|
+
return 1
|
|
133
|
+
|
|
134
|
+
for at, event in handler.events:
|
|
135
|
+
print(f" wf_{event}_ms={ms(t0)}")
|
|
136
|
+
if event == "message.delta":
|
|
137
|
+
break
|
|
138
|
+
if event in ("error", "concierge"):
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
raise SystemExit(main())
|