dulus 0.2.0__py3-none-any.whl
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.
- agent.py +363 -0
- backend/__init__.py +63 -0
- backend/compressor.py +261 -0
- backend/context.py +329 -0
- backend/githook.py +166 -0
- backend/marketplace.py +141 -0
- backend/mempalace_bridge.py +182 -0
- backend/personas.py +297 -0
- backend/plugins.py +222 -0
- backend/server.py +411 -0
- backend/tasks.py +213 -0
- batch_api.py +307 -0
- checkpoint/__init__.py +27 -0
- checkpoint/hooks.py +90 -0
- checkpoint/store.py +314 -0
- checkpoint/types.py +80 -0
- claude_code_watcher.py +214 -0
- clipboard_utils.py +246 -0
- cloudsave.py +159 -0
- common.py +177 -0
- compaction.py +378 -0
- config.py +180 -0
- context.py +241 -0
- dulus-0.2.0.dist-info/METADATA +600 -0
- dulus-0.2.0.dist-info/RECORD +101 -0
- dulus-0.2.0.dist-info/WHEEL +5 -0
- dulus-0.2.0.dist-info/entry_points.txt +2 -0
- dulus-0.2.0.dist-info/licenses/LICENSE +674 -0
- dulus-0.2.0.dist-info/licenses/license_manager.py +187 -0
- dulus-0.2.0.dist-info/top_level.txt +36 -0
- dulus.py +8455 -0
- dulus_gui.py +331 -0
- dulus_mcp/__init__.py +43 -0
- dulus_mcp/client.py +546 -0
- dulus_mcp/config.py +133 -0
- dulus_mcp/tools.py +131 -0
- dulus_mcp/types.py +124 -0
- gui/__init__.py +18 -0
- gui/agent_bridge.py +283 -0
- gui/chat_widget.py +448 -0
- gui/main_window.py +485 -0
- gui/personas.py +230 -0
- gui/session_utils.py +189 -0
- gui/settings_dialog.py +146 -0
- gui/sidebar.py +515 -0
- gui/tasks_view.py +499 -0
- gui/themes.py +256 -0
- gui/tool_panel.py +94 -0
- input.py +1030 -0
- license_manager.py +187 -0
- memory/__init__.py +93 -0
- memory/audit.py +51 -0
- memory/consolidator.py +312 -0
- memory/context.py +270 -0
- memory/offload.py +148 -0
- memory/palace.py +127 -0
- memory/scan.py +146 -0
- memory/sessions.py +100 -0
- memory/store.py +395 -0
- memory/tools.py +408 -0
- memory/types.py +114 -0
- memory/vector_search.py +92 -0
- multi_agent/__init__.py +23 -0
- multi_agent/subagent.py +501 -0
- multi_agent/tools.py +393 -0
- offload_helper.py +183 -0
- plugin/__init__.py +22 -0
- plugin/autoadapter.py +1641 -0
- plugin/loader.py +156 -0
- plugin/recommend.py +211 -0
- plugin/store.py +387 -0
- plugin/types.py +147 -0
- providers.py +3750 -0
- skill/__init__.py +14 -0
- skill/builtin.py +100 -0
- skill/clawhub.py +270 -0
- skill/executor.py +66 -0
- skill/loader.py +199 -0
- skill/tools.py +110 -0
- skills.py +14 -0
- spinner.py +42 -0
- string_utils.py +42 -0
- subagent.py +11 -0
- task/__init__.py +12 -0
- task/store.py +199 -0
- task/tools.py +265 -0
- task/types.py +92 -0
- tmux_offloader.py +177 -0
- tmux_tools.py +410 -0
- tool_registry.py +214 -0
- tools.py +2694 -0
- ui/__init__.py +1 -0
- ui/input.py +464 -0
- ui/render.py +272 -0
- voice/__init__.py +56 -0
- voice/keyterms.py +179 -0
- voice/recorder.py +263 -0
- voice/stt.py +408 -0
- voice/tts.py +570 -0
- webchat.py +432 -0
- webchat_server.py +1761 -0
webchat.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Dulus WebChat — standalone or in-process mirror of the terminal agent.
|
|
2
|
+
|
|
3
|
+
When launched via /webchat from backend.py, the in-process server in
|
|
4
|
+
webchat_server.py is used instead. This file remains usable as a
|
|
5
|
+
standalone fallback.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import queue
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
import webbrowser
|
|
16
|
+
from typing import Generator
|
|
17
|
+
|
|
18
|
+
from flask import Flask, request, jsonify, Response, stream_with_context
|
|
19
|
+
|
|
20
|
+
from agent import (
|
|
21
|
+
run as agent_run,
|
|
22
|
+
AgentState,
|
|
23
|
+
TextChunk,
|
|
24
|
+
ThinkingChunk,
|
|
25
|
+
ToolStart,
|
|
26
|
+
ToolEnd,
|
|
27
|
+
TurnDone,
|
|
28
|
+
PermissionRequest,
|
|
29
|
+
)
|
|
30
|
+
from context import build_system_prompt
|
|
31
|
+
from common import sanitize_text
|
|
32
|
+
from config import load_config
|
|
33
|
+
|
|
34
|
+
# Ensure tools are registered
|
|
35
|
+
import tools as _tools_init
|
|
36
|
+
import memory.tools as _mem_tools_init
|
|
37
|
+
import multi_agent.tools as _ma_tools_init
|
|
38
|
+
import skill.tools as _sk_tools_init
|
|
39
|
+
import dulus_mcp.tools as _mcp_tools_init
|
|
40
|
+
import task.tools as _task_tools_init
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import tmux_tools as _tmux_tools_init
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from plugin.loader import register_plugin_tools
|
|
49
|
+
register_plugin_tools()
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
# ── shared state for standalone mode ───────────────────────────────────────
|
|
54
|
+
HISTORY_LOCK = threading.Lock()
|
|
55
|
+
CONFIG = load_config()
|
|
56
|
+
STATE = AgentState()
|
|
57
|
+
_PENDING_PERMISSIONS: dict[str, tuple[PermissionRequest, threading.Event]] = {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _run_agent_standalone(user_message: str) -> Generator:
|
|
61
|
+
"""Run agent loop with local state/config, yielding all events."""
|
|
62
|
+
cfg = CONFIG
|
|
63
|
+
state = STATE
|
|
64
|
+
user_input = sanitize_text(user_message)
|
|
65
|
+
|
|
66
|
+
# Skill inject
|
|
67
|
+
_skill_body = cfg.pop("_skill_inject", "")
|
|
68
|
+
if _skill_body:
|
|
69
|
+
user_input = (
|
|
70
|
+
"[SKILL CONTEXT — follow these instructions for this turn]\n\n"
|
|
71
|
+
+ _skill_body
|
|
72
|
+
+ "\n\n---\n\n[USER MESSAGE]\n"
|
|
73
|
+
+ user_input
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# MemPalace
|
|
77
|
+
if cfg.get("mem_palace", True) and user_input and len(user_input.strip()) >= 12:
|
|
78
|
+
_trivial = {
|
|
79
|
+
"hola", "klk", "gracias", "ok", "si", "no", "dale",
|
|
80
|
+
"exit", "quit", "help", "thanks", "bien",
|
|
81
|
+
}
|
|
82
|
+
_first = user_input.strip().lower().split()[0]
|
|
83
|
+
if _first not in _trivial:
|
|
84
|
+
try:
|
|
85
|
+
from memory import find_relevant_memories
|
|
86
|
+
|
|
87
|
+
_q = user_input.strip()[:200]
|
|
88
|
+
_raw_hits = find_relevant_memories(_q, max_results=3)
|
|
89
|
+
if _raw_hits:
|
|
90
|
+
_parts = []
|
|
91
|
+
for _i, _h in enumerate(_raw_hits, 1):
|
|
92
|
+
_name = _h.get("name", f"hit_{_i}")
|
|
93
|
+
_desc = _h.get("description", "")
|
|
94
|
+
_body = _h.get("content", "").strip()
|
|
95
|
+
_snip = _body[:300] + ("..." if len(_body) > 300 else "")
|
|
96
|
+
if _desc:
|
|
97
|
+
_parts.append(f"### {_name}\n_{_desc}_\n{_snip}")
|
|
98
|
+
else:
|
|
99
|
+
_parts.append(f"### {_name}\n{_snip}")
|
|
100
|
+
_hits_str = "\n\n".join(_parts)
|
|
101
|
+
if len(_hits_str) > 2000:
|
|
102
|
+
_hits_str = _hits_str[:2000] + "\n[...truncated]"
|
|
103
|
+
_inject = (
|
|
104
|
+
"[MemPalace — relevant memories pre-loaded for this turn. "
|
|
105
|
+
"Do NOT re-query unless the user explicitly asks for more.]\n\n"
|
|
106
|
+
+ _hits_str
|
|
107
|
+
)
|
|
108
|
+
user_input = (
|
|
109
|
+
_inject + "\n\n---\n\n[USER MESSAGE]\n" + user_input
|
|
110
|
+
)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
system_prompt = build_system_prompt(cfg)
|
|
115
|
+
cfg["_last_interaction_time"] = time.time()
|
|
116
|
+
|
|
117
|
+
yield from agent_run(user_input, state, cfg, system_prompt)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def create_app() -> Flask:
|
|
121
|
+
app = Flask(__name__)
|
|
122
|
+
|
|
123
|
+
PAGE = """<!doctype html>
|
|
124
|
+
<html lang="es"><head><meta charset="utf-8"><title>Dulus WebChat</title>
|
|
125
|
+
<style>
|
|
126
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
127
|
+
body{background:#0a0a0c;color:#e6e6e6;font:14px/1.5 Consolas,monospace;height:100vh;display:flex;flex-direction:column}
|
|
128
|
+
header{padding:10px 18px;background:#111;border-bottom:1px solid #222;display:flex;justify-content:space-between;align-items:center}
|
|
129
|
+
header h1{font-size:14px;color:#00ffa3;font-weight:600}
|
|
130
|
+
header .model{font-size:11px;color:#666}
|
|
131
|
+
header button{background:#222;color:#888;border:1px solid #333;padding:4px 10px;border-radius:3px;cursor:pointer;font:inherit}
|
|
132
|
+
header button:hover{color:#fff}
|
|
133
|
+
#log{flex:1;overflow-y:auto;padding:18px;display:flex;flex-direction:column;gap:14px}
|
|
134
|
+
.msg{max-width:780px;padding:10px 14px;border-radius:6px;white-space:pre-wrap;word-wrap:break-word}
|
|
135
|
+
.user{align-self:flex-end;background:#1a3a2a;border:1px solid #2a5a40}
|
|
136
|
+
.assistant{align-self:flex-start;background:#15151a;border:1px solid #2a2a30}
|
|
137
|
+
.meta{font-size:10px;color:#555;margin-top:4px}
|
|
138
|
+
.err{color:#ff6b6b}
|
|
139
|
+
form{display:flex;gap:8px;padding:12px 18px;background:#111;border-top:1px solid #222}
|
|
140
|
+
textarea{flex:1;background:#000;color:#e6e6e6;border:1px solid #333;padding:10px;border-radius:4px;font:inherit;resize:none;height:60px;outline:none}
|
|
141
|
+
textarea:focus{border-color:#00ffa3}
|
|
142
|
+
button.send{background:#00ffa3;color:#000;border:none;padding:0 22px;border-radius:4px;font-weight:600;cursor:pointer}
|
|
143
|
+
button.send:disabled{opacity:.4;cursor:wait}
|
|
144
|
+
.think{font-size:10px;color:#888;margin-top:6px;padding:6px;border-left:2px solid #444;background:#0d0d10;white-space:pre-wrap}
|
|
145
|
+
.tool{font-size:11px;color:#aaa;margin-top:6px;padding:8px;border-left:2px solid #00ffa3;background:#0d1a14;white-space:pre-wrap}
|
|
146
|
+
.tool-result{font-size:11px;color:#ccc;margin-top:4px;padding:6px;border-left:2px solid #444;background:#111;white-space:pre-wrap;max-height:200px;overflow-y:auto}
|
|
147
|
+
.perm{font-size:12px;color:#ffcc00;margin-top:6px;padding:8px;border:1px solid #443300;background:#1a1500;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
148
|
+
.perm button{background:#333;color:#fff;border:1px solid #555;padding:4px 12px;border-radius:3px;cursor:pointer}
|
|
149
|
+
.perm button.approve{background:#00ffa3;color:#000;border-color:#00ffa3}
|
|
150
|
+
</style></head><body>
|
|
151
|
+
<header><h1>DULUS WEBCHAT</h1><span class="model" id="modelTag">…</span><button onclick="clearChat()">clear</button></header>
|
|
152
|
+
<div id="log"></div>
|
|
153
|
+
<form id="f" onsubmit="return send(event)">
|
|
154
|
+
<textarea id="inp" placeholder="Mensaje a Dulus... (Enter envía, Shift+Enter nueva línea)" autofocus></textarea>
|
|
155
|
+
<button class="send" id="sendBtn">SEND</button>
|
|
156
|
+
</form>
|
|
157
|
+
<script>
|
|
158
|
+
const log=document.getElementById('log'),inp=document.getElementById('inp'),btn=document.getElementById('sendBtn'),modelTag=document.getElementById('modelTag');
|
|
159
|
+
|
|
160
|
+
function add(role,text,extra){
|
|
161
|
+
const d=document.createElement('div');d.className='msg '+role;
|
|
162
|
+
if(typeof text==='string') d.textContent=text; else d.appendChild(text);
|
|
163
|
+
if(extra){const m=document.createElement('div');m.className='meta';m.textContent=extra;d.appendChild(m);}
|
|
164
|
+
log.appendChild(d);log.scrollTop=log.scrollHeight;return d;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let currentAssistant=null, currentText='';
|
|
168
|
+
|
|
169
|
+
function ensureAssistant(){
|
|
170
|
+
if(!currentAssistant){currentAssistant=add('assistant','');}
|
|
171
|
+
return currentAssistant;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function appendText(text){
|
|
175
|
+
ensureAssistant();
|
|
176
|
+
currentText+=text;
|
|
177
|
+
currentAssistant.textContent=currentText;
|
|
178
|
+
log.scrollTop=log.scrollHeight;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function appendThinking(text){
|
|
182
|
+
ensureAssistant();
|
|
183
|
+
let th=currentAssistant.querySelector('.think');
|
|
184
|
+
if(!th){th=document.createElement('div');th.className='think';th.textContent='[thinking]\n';currentAssistant.appendChild(th);}
|
|
185
|
+
th.textContent+=text;
|
|
186
|
+
log.scrollTop=log.scrollHeight;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function startTool(name,inputs){
|
|
190
|
+
ensureAssistant();
|
|
191
|
+
const t=document.createElement('div');t.className='tool';
|
|
192
|
+
t.textContent='🔧 '+name+'\n'+JSON.stringify(inputs,null,2);
|
|
193
|
+
currentAssistant.appendChild(t);
|
|
194
|
+
log.scrollTop=log.scrollHeight;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function endTool(name,result,permitted){
|
|
198
|
+
ensureAssistant();
|
|
199
|
+
const r=document.createElement('div');r.className='tool-result';
|
|
200
|
+
r.textContent=(permitted?'✅':'❌')+' '+result;
|
|
201
|
+
currentAssistant.appendChild(r);
|
|
202
|
+
log.scrollTop=log.scrollHeight;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function showPermission(id,desc){
|
|
206
|
+
ensureAssistant();
|
|
207
|
+
const p=document.createElement('div');p.className='perm';
|
|
208
|
+
p.innerHTML='<span>⛔ '+desc+'</span>';
|
|
209
|
+
const yes=document.createElement('button');yes.textContent='Approve';yes.className='approve';
|
|
210
|
+
yes.onclick=()=>{sendPermission(id,true);p.remove();};
|
|
211
|
+
const no=document.createElement('button');no.textContent='Deny';
|
|
212
|
+
no.onclick=()=>{sendPermission(id,false);p.remove();};
|
|
213
|
+
p.appendChild(yes);p.appendChild(no);
|
|
214
|
+
currentAssistant.appendChild(p);
|
|
215
|
+
log.scrollTop=log.scrollHeight;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function sendPermission(id,granted){
|
|
219
|
+
await fetch('/permission',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,granted})});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function send(e){
|
|
223
|
+
if(e)e.preventDefault();
|
|
224
|
+
const t=inp.value.trim();if(!t)return false;
|
|
225
|
+
add('user',t);inp.value='';btn.disabled=true;
|
|
226
|
+
currentAssistant=null;currentText='';
|
|
227
|
+
try{
|
|
228
|
+
const resp=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:t})});
|
|
229
|
+
const reader=resp.body.getReader();
|
|
230
|
+
const decoder=new TextDecoder();
|
|
231
|
+
let buf='';
|
|
232
|
+
while(true){
|
|
233
|
+
const {done,value}=await reader.read();
|
|
234
|
+
if(done) break;
|
|
235
|
+
buf+=decoder.decode(value,{stream:true});
|
|
236
|
+
const lines=buf.split('\n');
|
|
237
|
+
buf=lines.pop();
|
|
238
|
+
for(const line of lines){
|
|
239
|
+
if(!line.startsWith('data: ')) continue;
|
|
240
|
+
let d;
|
|
241
|
+
try{d=JSON.parse(line.slice(6));}catch(_){continue;}
|
|
242
|
+
if(d.type==='text') appendText(d.text);
|
|
243
|
+
else if(d.type==='thinking') appendThinking(d.text);
|
|
244
|
+
else if(d.type==='tool_start') startTool(d.name,d.inputs);
|
|
245
|
+
else if(d.type==='tool_end') endTool(d.name,d.result,d.permitted);
|
|
246
|
+
else if(d.type==='permission') showPermission(d.id,d.description);
|
|
247
|
+
else if(d.type==='turn_done'){
|
|
248
|
+
const meta=document.createElement('div');meta.className='meta';
|
|
249
|
+
meta.textContent='in:'+d.in+' out:'+d.out;
|
|
250
|
+
ensureAssistant().appendChild(meta);
|
|
251
|
+
}
|
|
252
|
+
else if(d.type==='error') appendText('\n[error] '+d.message);
|
|
253
|
+
else if(d.type==='done'){}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}catch(err){
|
|
257
|
+
add('assistant','[network] '+err,'').classList.add('err');
|
|
258
|
+
}finally{
|
|
259
|
+
btn.disabled=false;inp.focus();
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function clearChat(){
|
|
265
|
+
await fetch('/clear',{method:'POST'});
|
|
266
|
+
log.innerHTML='';currentAssistant=null;currentText='';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function syncChat(){
|
|
270
|
+
if(btn.disabled) return;
|
|
271
|
+
const r=await fetch('/state');const j=await r.json();
|
|
272
|
+
modelTag.textContent=j.model;
|
|
273
|
+
const currentMsgs = log.querySelectorAll('.msg').length;
|
|
274
|
+
if(j.history.length !== currentMsgs){
|
|
275
|
+
const wasNearBottom = log.scrollTop + log.clientHeight >= log.scrollHeight - 50;
|
|
276
|
+
log.innerHTML='';
|
|
277
|
+
currentAssistant=null;
|
|
278
|
+
currentText='';
|
|
279
|
+
for(const m of j.history){
|
|
280
|
+
if(m.role==='user') add('user',m.content||'');
|
|
281
|
+
else if(m.role==='assistant'){
|
|
282
|
+
const d=add('assistant',m.content||'');
|
|
283
|
+
if(m.thinking){const th=document.createElement('div');th.className='think';th.textContent='[thinking]\n'+m.thinking;d.appendChild(th);}
|
|
284
|
+
}
|
|
285
|
+
else if(m.role==='tool'){
|
|
286
|
+
const d=add('assistant','');
|
|
287
|
+
const t=document.createElement('div');t.className='tool-result';t.textContent='🔧 tool result:\n'+(m.content||'');d.appendChild(t);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if(wasNearBottom) log.scrollTop=log.scrollHeight;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function loadHist(){ return syncChat(); }
|
|
295
|
+
|
|
296
|
+
inp.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}});
|
|
297
|
+
loadHist();
|
|
298
|
+
setInterval(syncChat, 5000);
|
|
299
|
+
</script>
|
|
300
|
+
</body></html>"""
|
|
301
|
+
|
|
302
|
+
@app.route("/")
|
|
303
|
+
def home() -> Response:
|
|
304
|
+
return Response(PAGE, mimetype="text/html")
|
|
305
|
+
|
|
306
|
+
@app.route("/state")
|
|
307
|
+
def state_endpoint() -> Response:
|
|
308
|
+
with HISTORY_LOCK:
|
|
309
|
+
hist = [dict(m) for m in STATE.messages]
|
|
310
|
+
model = CONFIG.get("model", "?")
|
|
311
|
+
return jsonify(model=model, history=hist)
|
|
312
|
+
|
|
313
|
+
@app.route("/clear", methods=["POST"])
|
|
314
|
+
def clear() -> Response:
|
|
315
|
+
with HISTORY_LOCK:
|
|
316
|
+
STATE.messages.clear()
|
|
317
|
+
return jsonify(ok=True)
|
|
318
|
+
|
|
319
|
+
@app.route("/permission", methods=["POST"])
|
|
320
|
+
def permission() -> Response:
|
|
321
|
+
body = request.get_json(silent=True) or {}
|
|
322
|
+
pid = body.get("id")
|
|
323
|
+
granted = body.get("granted", False)
|
|
324
|
+
with HISTORY_LOCK:
|
|
325
|
+
item = _PENDING_PERMISSIONS.get(pid)
|
|
326
|
+
if item is None:
|
|
327
|
+
return jsonify(error="not found"), 404
|
|
328
|
+
req, evt = item
|
|
329
|
+
req.granted = bool(granted)
|
|
330
|
+
evt.set()
|
|
331
|
+
return jsonify(ok=True)
|
|
332
|
+
|
|
333
|
+
@app.route("/chat", methods=["POST"])
|
|
334
|
+
def chat() -> Response:
|
|
335
|
+
body = request.get_json(silent=True) or {}
|
|
336
|
+
msg = (body.get("message") or "").strip()
|
|
337
|
+
if not msg:
|
|
338
|
+
return jsonify(error="empty message"), 400
|
|
339
|
+
|
|
340
|
+
def generate():
|
|
341
|
+
q: queue.Queue = queue.Queue(maxsize=512)
|
|
342
|
+
exc_holder = [None]
|
|
343
|
+
|
|
344
|
+
def producer():
|
|
345
|
+
try:
|
|
346
|
+
for ev in _run_agent_standalone(msg):
|
|
347
|
+
q.put(ev)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
exc_holder[0] = e
|
|
350
|
+
finally:
|
|
351
|
+
q.put(None)
|
|
352
|
+
|
|
353
|
+
t = threading.Thread(target=producer, daemon=True)
|
|
354
|
+
t.start()
|
|
355
|
+
|
|
356
|
+
yield 'data: {"type":"start"}\n\n'
|
|
357
|
+
|
|
358
|
+
while True:
|
|
359
|
+
item = q.get()
|
|
360
|
+
if item is None:
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
payload = None
|
|
364
|
+
if isinstance(item, TextChunk):
|
|
365
|
+
payload = {"type": "text", "text": item.text}
|
|
366
|
+
elif isinstance(item, ThinkingChunk):
|
|
367
|
+
payload = {"type": "thinking", "text": item.text}
|
|
368
|
+
elif isinstance(item, ToolStart):
|
|
369
|
+
payload = {"type": "tool_start", "name": item.name, "inputs": item.inputs}
|
|
370
|
+
elif isinstance(item, ToolEnd):
|
|
371
|
+
payload = {
|
|
372
|
+
"type": "tool_end",
|
|
373
|
+
"name": item.name,
|
|
374
|
+
"result": item.result,
|
|
375
|
+
"permitted": item.permitted,
|
|
376
|
+
}
|
|
377
|
+
elif isinstance(item, TurnDone):
|
|
378
|
+
payload = {
|
|
379
|
+
"type": "turn_done",
|
|
380
|
+
"in": item.input_tokens,
|
|
381
|
+
"out": item.output_tokens,
|
|
382
|
+
}
|
|
383
|
+
elif isinstance(item, PermissionRequest):
|
|
384
|
+
pid = str(uuid.uuid4())
|
|
385
|
+
evt = threading.Event()
|
|
386
|
+
_PENDING_PERMISSIONS[pid] = (item, evt)
|
|
387
|
+
payload = {
|
|
388
|
+
"type": "permission",
|
|
389
|
+
"id": pid,
|
|
390
|
+
"description": item.description,
|
|
391
|
+
}
|
|
392
|
+
yield f"data: {json.dumps(payload)}\n\n"
|
|
393
|
+
evt.wait(timeout=300)
|
|
394
|
+
_PENDING_PERMISSIONS.pop(pid, None)
|
|
395
|
+
continue
|
|
396
|
+
else:
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
yield f"data: {json.dumps(payload)}\n\n"
|
|
400
|
+
|
|
401
|
+
if exc_holder[0]:
|
|
402
|
+
err = exc_holder[0]
|
|
403
|
+
yield f'data: {json.dumps({"type":"error","message":f"{type(err).__name__}: {err}"})}\n\n'
|
|
404
|
+
|
|
405
|
+
yield 'data: {"type":"done"}\n\n'
|
|
406
|
+
|
|
407
|
+
return Response(
|
|
408
|
+
stream_with_context(generate()),
|
|
409
|
+
mimetype="text/event-stream",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return app
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def main():
|
|
416
|
+
ap = argparse.ArgumentParser()
|
|
417
|
+
ap.add_argument("--port", type=int, default=5000)
|
|
418
|
+
ap.add_argument("--host", default="0.0.0.0")
|
|
419
|
+
ap.add_argument("--model", default="", help="Override model from config.json")
|
|
420
|
+
ap.add_argument("--open", action="store_true", help="open browser on start")
|
|
421
|
+
args = ap.parse_args()
|
|
422
|
+
if args.model:
|
|
423
|
+
CONFIG["model"] = args.model
|
|
424
|
+
app = create_app()
|
|
425
|
+
if args.open:
|
|
426
|
+
threading.Timer(1.0, lambda: webbrowser.open(f"http://{args.host}:{args.port}/")).start()
|
|
427
|
+
print(f"[webchat] model={CONFIG.get('model')} -> http://{args.host}:{args.port}/")
|
|
428
|
+
app.run(host=args.host, port=args.port, debug=False, use_reloader=False, threaded=True)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
if __name__ == "__main__":
|
|
432
|
+
main()
|