muralipala1504 commited on
Commit
58ab20c
·
1 Parent(s): 06fa89e

Add Phase 1 command execution: SSE streaming + Execute button with safety checks

Browse files
app.js CHANGED
@@ -1,4 +1,4 @@
1
- // Plain Vanilla Chat Frontend with Copy Button
2
  (function () {
3
  "use strict";
4
 
@@ -24,12 +24,10 @@
24
  btn.textContent = "Copy";
25
  btn.onclick = async () => {
26
  try {
27
- // Try modern clipboard API first
28
  await navigator.clipboard.writeText(code);
29
  btn.textContent = "Copied!";
30
  setTimeout(() => { btn.textContent = "Copy"; }, 2000);
31
  } catch (err) {
32
- // Fallback for non-secure contexts
33
  const textarea = document.createElement("textarea");
34
  textarea.value = code;
35
  textarea.style.position = "fixed";
@@ -50,53 +48,140 @@
50
  return btn;
51
  }
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  function appendMessage(container, sender, message, useMarkdown = false) {
54
  const msgDiv = document.createElement("div");
55
  msgDiv.classList.add("message");
56
 
57
- const codeBlockMatch =
58
- typeof message === "string"
59
- ? message.match(/```(\w+)?\n([\s\S]*?)```/)
60
- : null;
61
-
62
- if (codeBlockMatch) {
63
- const lang = codeBlockMatch[1] || "bash";
64
- const code = codeBlockMatch[2];
65
 
66
- const codeWrapper = document.createElement("div");
67
- codeWrapper.className = "code-wrapper";
68
 
69
- const pre = document.createElement("pre");
70
- const codeEl = document.createElement("code");
71
- codeEl.className = `language-${lang}`;
72
- codeEl.textContent = code;
73
- pre.appendChild(codeEl);
74
-
75
- const copyBtn = createCopyButton(code);
76
-
77
- codeWrapper.appendChild(copyBtn);
78
- codeWrapper.appendChild(pre);
79
-
80
- msgDiv.innerHTML = `<strong>${sender}:</strong>`;
81
- msgDiv.appendChild(codeWrapper);
82
- } else if (useMarkdown) {
83
- msgDiv.innerHTML = `<strong>${sender}:</strong>${renderMarkdown(String(message))}`;
84
-
85
- // Add copy buttons to any code blocks in markdown
86
- msgDiv.querySelectorAll("pre code").forEach((codeEl) => {
87
- const code = codeEl.textContent;
88
- const pre = codeEl.parentElement;
89
- const wrapper = document.createElement("div");
90
- wrapper.className = "code-wrapper";
91
 
92
- const copyBtn = createCopyButton(code);
 
 
 
 
 
93
 
94
- pre.parentNode.insertBefore(wrapper, pre);
95
- wrapper.appendChild(copyBtn);
96
- wrapper.appendChild(pre);
 
 
 
 
 
 
97
  });
98
  } else {
99
- msgDiv.innerHTML = `<strong>${sender}:</strong> <pre>${escapeHtml(String(message))}</pre>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
 
102
  container.appendChild(msgDiv);
@@ -105,9 +190,102 @@
105
  if (window.Prism) {
106
  try {
107
  Prism.highlightAllUnder(container);
108
- } catch (_) {
109
- // no-op if Prism not available
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
112
  }
113
 
@@ -123,44 +301,6 @@
123
  return;
124
  }
125
 
126
- async function sendPrompt(prompt) {
127
- try {
128
- const response = await fetch("/chat/run-agent", {
129
- method: "POST",
130
- headers: { "Content-Type": "application/json" },
131
- body: JSON.stringify({ prompt }),
132
- });
133
-
134
- let data = null;
135
- let rawText = "";
136
- try {
137
- rawText = await response.clone().text();
138
- data = JSON.parse(rawText);
139
- } catch {
140
- if (!response.ok) {
141
- appendMessage(output, "Error", `Server error: ${response.status} ${response.statusText}\n${rawText || "(no body)"}`);
142
- return;
143
- }
144
- appendMessage(output, "Error", `Bad JSON response (${response.status} ${response.statusText})`);
145
- return;
146
- }
147
-
148
- if (!response.ok || data.error) {
149
- const errText = data.error || `${response.status} ${response.statusText}`;
150
- appendMessage(output, "Error", errText);
151
- return;
152
- }
153
-
154
- const text = data.answer ?? data.output ?? "No response";
155
- appendMessage(output, "DeepShell", text, true);
156
- } catch (err) {
157
- appendMessage(output, "Error", `Network error: ${err.message}`);
158
- } finally {
159
- if (sendBtn) sendBtn.disabled = false;
160
- input.focus();
161
- }
162
- }
163
-
164
  form.addEventListener("submit", (e) => {
165
  e.preventDefault();
166
  const prompt = input.value.trim();
@@ -170,7 +310,8 @@
170
  input.value = "";
171
  if (sendBtn) sendBtn.disabled = true;
172
 
173
- sendPrompt(prompt);
 
174
  });
175
 
176
  if (sendBtn) {
 
1
+ // Plain Vanilla Chat Frontend with Copy Button, SSE Streaming, and Command Execution
2
  (function () {
3
  "use strict";
4
 
 
24
  btn.textContent = "Copy";
25
  btn.onclick = async () => {
26
  try {
 
27
  await navigator.clipboard.writeText(code);
28
  btn.textContent = "Copied!";
29
  setTimeout(() => { btn.textContent = "Copy"; }, 2000);
30
  } catch (err) {
 
31
  const textarea = document.createElement("textarea");
32
  textarea.value = code;
33
  textarea.style.position = "fixed";
 
48
  return btn;
49
  }
50
 
51
+ function createExecuteButton(command, output) {
52
+ const btn = document.createElement("button");
53
+ btn.className = "execute-btn";
54
+ btn.textContent = "Execute";
55
+ btn.onclick = async () => {
56
+ btn.disabled = true;
57
+ btn.textContent = "Executing...";
58
+
59
+ try {
60
+ const response = await fetch("/chat/execute", {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({ command }),
64
+ });
65
+
66
+ const result = await response.json();
67
+
68
+ if (result.success) {
69
+ const outputText = result.stdout || result.stderr || "(no output)";
70
+ appendMessage(output, "Command Output", `Exit Code: ${result.exit_code}\n\n${outputText}`, false);
71
+ btn.textContent = "✓ Executed";
72
+ btn.style.background = "#22c55e";
73
+ } else {
74
+ appendMessage(output, "Execution Error", result.error || "Unknown error", false);
75
+ btn.textContent = "✗ Failed";
76
+ btn.style.background = "#ef4444";
77
+ }
78
+ } catch (err) {
79
+ appendMessage(output, "Error", `Failed to execute: ${err.message}`, false);
80
+ btn.textContent = "✗ Error";
81
+ btn.style.background = "#ef4444";
82
+ }
83
+
84
+ setTimeout(() => {
85
+ btn.disabled = false;
86
+ btn.textContent = "Execute";
87
+ btn.style.background = "";
88
+ }, 3000);
89
+ };
90
+ return btn;
91
+ }
92
+
93
+ function extractExecuteTags(text) {
94
+ const regex = /<execute>(.*?)<\/execute>/gs;
95
+ const commands = [];
96
+ let match;
97
+
98
+ while ((match = regex.exec(text)) !== null) {
99
+ commands.push(match[1].trim());
100
+ }
101
+
102
+ return commands;
103
+ }
104
+
105
  function appendMessage(container, sender, message, useMarkdown = false) {
106
  const msgDiv = document.createElement("div");
107
  msgDiv.classList.add("message");
108
 
109
+ // Check for execute tags
110
+ const commands = extractExecuteTags(message);
111
+
112
+ if (commands.length > 0) {
113
+ // Remove execute tags from display
114
+ let displayText = message.replace(/<execute>.*?<\/execute>/gs, "");
 
 
115
 
116
+ msgDiv.innerHTML = `<strong>${sender}:</strong>${useMarkdown ? renderMarkdown(displayText) : `<pre>${escapeHtml(displayText)}</pre>`}`;
 
117
 
118
+ // Add execute buttons for each command
119
+ commands.forEach(cmd => {
120
+ const cmdWrapper = document.createElement("div");
121
+ cmdWrapper.className = "command-wrapper";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ const cmdPre = document.createElement("pre");
124
+ cmdPre.className = "command-block";
125
+ cmdPre.textContent = cmd;
126
+
127
+ const btnContainer = document.createElement("div");
128
+ btnContainer.className = "command-buttons";
129
 
130
+ const executeBtn = createExecuteButton(cmd, container);
131
+ const copyBtn = createCopyButton(cmd);
132
+
133
+ btnContainer.appendChild(executeBtn);
134
+ btnContainer.appendChild(copyBtn);
135
+
136
+ cmdWrapper.appendChild(cmdPre);
137
+ cmdWrapper.appendChild(btnContainer);
138
+ msgDiv.appendChild(cmdWrapper);
139
  });
140
  } else {
141
+ // Regular message handling
142
+ const codeBlockMatch =
143
+ typeof message === "string"
144
+ ? message.match(/```(\w+)?\n([\s\S]*?)```/)
145
+ : null;
146
+
147
+ if (codeBlockMatch) {
148
+ const lang = codeBlockMatch[1] || "bash";
149
+ const code = codeBlockMatch[2];
150
+
151
+ const codeWrapper = document.createElement("div");
152
+ codeWrapper.className = "code-wrapper";
153
+
154
+ const pre = document.createElement("pre");
155
+ const codeEl = document.createElement("code");
156
+ codeEl.className = `language-${lang}`;
157
+ codeEl.textContent = code;
158
+ pre.appendChild(codeEl);
159
+
160
+ const copyBtn = createCopyButton(code);
161
+
162
+ codeWrapper.appendChild(copyBtn);
163
+ codeWrapper.appendChild(pre);
164
+
165
+ msgDiv.innerHTML = `<strong>${sender}:</strong>`;
166
+ msgDiv.appendChild(codeWrapper);
167
+ } else if (useMarkdown) {
168
+ msgDiv.innerHTML = `<strong>${sender}:</strong>${renderMarkdown(String(message))}`;
169
+
170
+ msgDiv.querySelectorAll("pre code").forEach((codeEl) => {
171
+ const code = codeEl.textContent;
172
+ const pre = codeEl.parentElement;
173
+ const wrapper = document.createElement("div");
174
+ wrapper.className = "code-wrapper";
175
+
176
+ const copyBtn = createCopyButton(code);
177
+
178
+ pre.parentNode.insertBefore(wrapper, pre);
179
+ wrapper.appendChild(copyBtn);
180
+ wrapper.appendChild(pre);
181
+ });
182
+ } else {
183
+ msgDiv.innerHTML = `<strong>${sender}:</strong> <pre>${escapeHtml(String(message))}</pre>`;
184
+ }
185
  }
186
 
187
  container.appendChild(msgDiv);
 
190
  if (window.Prism) {
191
  try {
192
  Prism.highlightAllUnder(container);
193
+ } catch (_) {}
194
+ }
195
+
196
+ return msgDiv;
197
+ }
198
+
199
+ // ---------- Streaming UI ----------
200
+ function createStreamingMessage(container, sender) {
201
+ const msgDiv = document.createElement("div");
202
+ msgDiv.classList.add("message");
203
+ msgDiv.innerHTML = `<strong>${sender}:</strong> <span class="streaming-content"></span>`;
204
+
205
+ const contentSpan = msgDiv.querySelector(".streaming-content");
206
+
207
+ container.appendChild(msgDiv);
208
+ container.scrollTop = container.scrollHeight;
209
+
210
+ return {
211
+ msgDiv,
212
+ contentSpan,
213
+ updateContent: (text) => {
214
+ contentSpan.textContent = text;
215
+ container.scrollTop = container.scrollHeight;
216
+ },
217
+ finalize: (text) => {
218
+ // Replace with properly formatted message
219
+ msgDiv.remove();
220
+ appendMessage(container, sender, text, true);
221
  }
222
+ };
223
+ }
224
+
225
+ // ---------- SSE Streaming ----------
226
+ async function sendPromptStreaming(prompt, output, sendBtn, input) {
227
+ const streamUI = createStreamingMessage(output, "DeepShell");
228
+ let fullText = "";
229
+
230
+ try {
231
+ const response = await fetch("/chat/run-agent-stream", {
232
+ method: "POST",
233
+ headers: { "Content-Type": "application/json" },
234
+ body: JSON.stringify({ prompt }),
235
+ });
236
+
237
+ if (!response.ok) {
238
+ throw new Error(`Server error: ${response.status}`);
239
+ }
240
+
241
+ const reader = response.body.getReader();
242
+ const decoder = new TextDecoder("utf-8");
243
+ let buffer = "";
244
+
245
+ while (true) {
246
+ const { done, value } = await reader.read();
247
+ if (done) break;
248
+
249
+ buffer += decoder.decode(value, { stream: true });
250
+
251
+ // SSE format: "data: {json}\n\n"
252
+ const lines = buffer.split("\n\n");
253
+ buffer = lines.pop(); // Keep incomplete data in buffer
254
+
255
+ for (const line of lines) {
256
+ if (!line.trim() || !line.startsWith("data: ")) continue;
257
+
258
+ const jsonStr = line.substring(6); // Remove "data: " prefix
259
+
260
+ try {
261
+ const data = JSON.parse(jsonStr);
262
+
263
+ if (data.type === "token") {
264
+ fullText += data.text;
265
+ streamUI.updateContent(fullText);
266
+ } else if (data.type === "done") {
267
+ streamUI.finalize(fullText);
268
+ return;
269
+ } else if (data.type === "error") {
270
+ throw new Error(data.message);
271
+ }
272
+ } catch (e) {
273
+ console.warn("Failed to parse SSE data:", jsonStr, e);
274
+ }
275
+ }
276
+ }
277
+
278
+ // Finalize with whatever we got
279
+ if (fullText) {
280
+ streamUI.finalize(fullText);
281
+ }
282
+ } catch (err) {
283
+ streamUI.msgDiv.remove();
284
+ appendMessage(output, "Error", `${err.message}`);
285
+ console.error("Streaming error:", err);
286
+ } finally {
287
+ if (sendBtn) sendBtn.disabled = false;
288
+ input.focus();
289
  }
290
  }
291
 
 
301
  return;
302
  }
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  form.addEventListener("submit", (e) => {
305
  e.preventDefault();
306
  const prompt = input.value.trim();
 
310
  input.value = "";
311
  if (sendBtn) sendBtn.disabled = true;
312
 
313
+ // Use streaming
314
+ sendPromptStreaming(prompt, output, sendBtn, input);
315
  });
316
 
317
  if (sendBtn) {
deepshell-backend/deepshell/__main__.py CHANGED
@@ -1,12 +1,17 @@
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.staticfiles import StaticFiles
4
- from fastapi.responses import FileResponse
5
  from pydantic import BaseModel
6
  import os
7
  from pathlib import Path
 
 
 
 
 
8
 
9
- from .llm import get_global_client # LLM enabled
10
 
11
  REPO_ROOT = Path(__file__).resolve().parents[2]
12
  INDEX_PATH = REPO_ROOT / "index.html"
@@ -35,6 +40,9 @@ app.mount("/static", StaticFiles(directory=str(STATIC_ROOT), html=False), name="
35
  class ChatRequest(BaseModel):
36
  prompt: str
37
 
 
 
 
38
  @app.get("/")
39
  def root_page():
40
  if INDEX_PATH.exists():
@@ -81,6 +89,202 @@ def run_agent(req: ChatRequest):
81
  text = str(resp)
82
  return {"answer": text}
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  if __name__ == "__main__":
85
  import uvicorn
86
  port = int(os.getenv("PORT", "8001"))
 
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
5
  from pydantic import BaseModel
6
  import os
7
  from pathlib import Path
8
+ import asyncio
9
+ import json
10
+ import subprocess
11
+ import shlex
12
+ from typing import Any, List
13
 
14
+ from .llm import get_global_client
15
 
16
  REPO_ROOT = Path(__file__).resolve().parents[2]
17
  INDEX_PATH = REPO_ROOT / "index.html"
 
40
  class ChatRequest(BaseModel):
41
  prompt: str
42
 
43
+ class ExecuteRequest(BaseModel):
44
+ command: str
45
+
46
  @app.get("/")
47
  def root_page():
48
  if INDEX_PATH.exists():
 
89
  text = str(resp)
90
  return {"answer": text}
91
 
92
+ # ============================================
93
+ # Command Execution Safety
94
+ # ============================================
95
+
96
+ # Whitelist of allowed command prefixes
97
+ ALLOWED_COMMANDS = [
98
+ "docker", "kubectl", "ls", "cat", "echo", "pwd", "whoami",
99
+ "df", "du", "ps", "top", "free", "uname", "date", "uptime",
100
+ "curl", "wget", "ping", "netstat", "ss", "ip", "ifconfig",
101
+ "git", "grep", "find", "head", "tail", "wc", "sort", "uniq"
102
+ ]
103
+
104
+ # Dangerous patterns to block
105
+ DANGEROUS_PATTERNS = [
106
+ "rm -rf", "rm -fr", "dd if=", "> /dev/", "mkfs", "fdisk",
107
+ "shutdown", "reboot", "init 0", "init 6", "halt", "poweroff",
108
+ ":(){ :|:& };:", "chmod -R 777", "chown -R"
109
+ ]
110
+
111
+ def is_command_safe(command: str) -> tuple[bool, str]:
112
+ """Check if command is safe to execute."""
113
+ cmd_lower = command.lower().strip()
114
+
115
+ # Check for dangerous patterns
116
+ for pattern in DANGEROUS_PATTERNS:
117
+ if pattern.lower() in cmd_lower:
118
+ return False, f"Dangerous pattern detected: {pattern}"
119
+
120
+ # Extract first word (command name)
121
+ try:
122
+ parts = shlex.split(command)
123
+ if not parts:
124
+ return False, "Empty command"
125
+
126
+ cmd_name = parts[0].split("/")[-1] # Handle /usr/bin/docker -> docker
127
+
128
+ # Check if command is in whitelist
129
+ if cmd_name not in ALLOWED_COMMANDS:
130
+ return False, f"Command '{cmd_name}' is not in the allowed list"
131
+
132
+ return True, "OK"
133
+ except Exception as e:
134
+ return False, f"Failed to parse command: {str(e)}"
135
+
136
+ def execute_command_safe(command: str, timeout: int = 30) -> dict:
137
+ """Execute command safely with timeout."""
138
+ try:
139
+ result = subprocess.run(
140
+ command,
141
+ shell=True,
142
+ capture_output=True,
143
+ text=True,
144
+ timeout=timeout,
145
+ cwd=os.getcwd(),
146
+ )
147
+
148
+ return {
149
+ "success": True,
150
+ "stdout": result.stdout,
151
+ "stderr": result.stderr,
152
+ "exit_code": result.returncode,
153
+ }
154
+ except subprocess.TimeoutExpired:
155
+ return {
156
+ "success": False,
157
+ "error": f"Command timed out after {timeout} seconds",
158
+ "stdout": "",
159
+ "stderr": "",
160
+ "exit_code": -1,
161
+ }
162
+ except Exception as e:
163
+ return {
164
+ "success": False,
165
+ "error": str(e),
166
+ "stdout": "",
167
+ "stderr": "",
168
+ "exit_code": -1,
169
+ }
170
+
171
+ # ============================================
172
+ # Execute Endpoint
173
+ # ============================================
174
+
175
+ @app.post("/chat/execute")
176
+ def execute_command(req: ExecuteRequest):
177
+ """Execute a command with safety checks."""
178
+ command = (req.command or "").strip()
179
+ if not command:
180
+ return JSONResponse(
181
+ {"success": False, "error": "Command is required"},
182
+ status_code=400
183
+ )
184
+
185
+ # Safety check
186
+ is_safe, reason = is_command_safe(command)
187
+ if not is_safe:
188
+ return JSONResponse(
189
+ {"success": False, "error": f"Command blocked: {reason}"},
190
+ status_code=403
191
+ )
192
+
193
+ # Execute
194
+ result = execute_command_safe(command)
195
+ return result
196
+
197
+ # ============================================
198
+ # SSE Streaming Endpoint
199
+ # ============================================
200
+
201
+ def _extract_text_from_chunk(chunk: Any) -> str:
202
+ """Extract text from streaming chunk - handles various LLM formats."""
203
+ try:
204
+ # Handle dict responses (OpenAI-like)
205
+ if isinstance(chunk, dict):
206
+ if "choices" in chunk and chunk["choices"]:
207
+ delta = chunk["choices"][0].get("delta", {})
208
+ return delta.get("content", "") or ""
209
+ return chunk.get("content", "") or chunk.get("text", "") or ""
210
+
211
+ # Handle object responses (SDK objects)
212
+ if hasattr(chunk, "choices") and chunk.choices:
213
+ choice = chunk.choices[0]
214
+ if hasattr(choice, "delta") and hasattr(choice.delta, "content"):
215
+ return choice.delta.content or ""
216
+ if hasattr(choice, "message") and hasattr(choice.message, "content"):
217
+ return choice.message.content or ""
218
+
219
+ return ""
220
+ except Exception:
221
+ return ""
222
+
223
+ async def _sse_generator(prompt: str):
224
+ """
225
+ SSE format generator: data: {json}\n\n
226
+ """
227
+ # Send start event
228
+ yield f"data: {json.dumps({'type': 'start'})}\n\n"
229
+
230
+ try:
231
+ client = get_global_client(os.getenv("PROVIDER", "groq"))
232
+ except Exception as e:
233
+ yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
234
+ yield f"data: {json.dumps({'type': 'done'})}\n\n"
235
+ return
236
+
237
+ try:
238
+ # Try streaming with stream=True
239
+ try:
240
+ response = client.chat(prompt, max_tokens=1024, temperature=0.2, stream=True)
241
+
242
+ # Handle streaming response
243
+ if hasattr(response, "__iter__"):
244
+ for chunk in response:
245
+ text = _extract_text_from_chunk(chunk)
246
+ if text:
247
+ yield f"data: {json.dumps({'type': 'token', 'text': text})}\n\n"
248
+ await asyncio.sleep(0) # Yield control to event loop
249
+ else:
250
+ # Not iterable, treat as single response
251
+ text = _extract_text_from_chunk(response)
252
+ if text:
253
+ yield f"data: {json.dumps({'type': 'token', 'text': text})}\n\n"
254
+
255
+ except TypeError:
256
+ # stream=True not supported, fallback to non-streaming
257
+ response = client.chat(prompt, max_tokens=1024, temperature=0.2)
258
+ try:
259
+ text = response.choices[0].message.content
260
+ except Exception:
261
+ text = str(response)
262
+
263
+ if text:
264
+ yield f"data: {json.dumps({'type': 'token', 'text': text})}\n\n"
265
+
266
+ except Exception as e:
267
+ yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
268
+
269
+ # Send done event
270
+ yield f"data: {json.dumps({'type': 'done'})}\n\n"
271
+
272
+ @app.post("/chat/run-agent-stream")
273
+ async def run_agent_stream(req: ChatRequest):
274
+ prompt = (req.prompt or "").strip()
275
+ if not prompt:
276
+ return JSONResponse({"error": "prompt is required"}, status_code=400)
277
+
278
+ return StreamingResponse(
279
+ _sse_generator(prompt),
280
+ media_type="text/event-stream",
281
+ headers={
282
+ "Cache-Control": "no-cache",
283
+ "Connection": "keep-alive",
284
+ "X-Accel-Buffering": "no",
285
+ }
286
+ )
287
+
288
  if __name__ == "__main__":
289
  import uvicorn
290
  port = int(os.getenv("PORT", "8001"))
deepshell-backend/deepshell/llm.py CHANGED
@@ -1,11 +1,30 @@
1
  import os
2
- from typing import Any
3
 
4
  # Minimal Groq client wrapper.
5
  from groq import Groq
6
 
7
  _singleton = {"client": None, "provider": None}
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  def get_global_client(provider: str = "groq") -> Any:
10
  provider = (provider or "groq").lower()
11
  if _singleton["client"] and _singleton["provider"] == provider:
@@ -21,13 +40,33 @@ def get_global_client(provider: str = "groq") -> Any:
21
  client = Groq(api_key=api_key)
22
 
23
  class Wrapper:
24
- def chat(self, prompt: str, max_tokens: int = 1024, temperature: float = 0.2):
 
 
 
 
 
 
 
25
  model = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
 
 
 
 
 
 
 
 
 
 
 
 
26
  response = client.chat.completions.create(
27
  model=model,
28
- messages=[{"role": "user", "content": prompt}],
29
  max_tokens=max_tokens,
30
  temperature=temperature,
 
31
  )
32
  return response
33
 
 
1
  import os
2
+ from typing import Any, Optional
3
 
4
  # Minimal Groq client wrapper.
5
  from groq import Groq
6
 
7
  _singleton = {"client": None, "provider": None}
8
 
9
+ DEEPSHELL_SYSTEM_PROMPT = """You are DeepShell, an AI assistant that helps users with Docker, Kubernetes, and system administration tasks.
10
+
11
+ When you recommend a command for the user to execute, wrap it in XML tags like this:
12
+ <execute>docker ps</execute>
13
+
14
+ Rules:
15
+ 1. Only suggest commands that are safe and relevant
16
+ 2. Explain what the command does before or after the execute tag
17
+ 3. One command per execute tag
18
+ 4. Use full command syntax (no placeholders like <container-id>)
19
+ 5. For multi-step tasks, provide commands one at a time
20
+
21
+ Example responses:
22
+ - "To see running containers, use: <execute>docker ps</execute>"
23
+ - "Let me check the disk usage: <execute>df -h</execute>"
24
+ - "Here's how to list all images: <execute>docker images</execute>"
25
+
26
+ Be helpful, concise, and always prioritize user safety."""
27
+
28
  def get_global_client(provider: str = "groq") -> Any:
29
  provider = (provider or "groq").lower()
30
  if _singleton["client"] and _singleton["provider"] == provider:
 
40
  client = Groq(api_key=api_key)
41
 
42
  class Wrapper:
43
+ def chat(
44
+ self,
45
+ prompt: str,
46
+ max_tokens: int = 1024,
47
+ temperature: float = 0.2,
48
+ stream: bool = False,
49
+ system_prompt: Optional[str] = None
50
+ ):
51
  model = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
52
+
53
+ messages = []
54
+
55
+ # Add system prompt if provided, otherwise use default
56
+ if system_prompt is not None:
57
+ if system_prompt: # Only add if not empty string
58
+ messages.append({"role": "system", "content": system_prompt})
59
+ else:
60
+ messages.append({"role": "system", "content": DEEPSHELL_SYSTEM_PROMPT})
61
+
62
+ messages.append({"role": "user", "content": prompt})
63
+
64
  response = client.chat.completions.create(
65
  model=model,
66
+ messages=messages,
67
  max_tokens=max_tokens,
68
  temperature=temperature,
69
+ stream=stream,
70
  )
71
  return response
72
 
index.html CHANGED
@@ -122,7 +122,7 @@
122
  }
123
  a { color: var(--accent); text-decoration: none; }
124
  a:hover { text-decoration: underline; }
125
-
126
  /* Copy button styles */
127
  .code-wrapper {
128
  position: relative;
@@ -146,6 +146,47 @@
146
  background: #334155;
147
  border-color: #475569;
148
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </style>
150
  </head>
151
  <body>
@@ -159,12 +200,12 @@
159
  <div id="chat-output" class="panel"></div>
160
 
161
  <form id="chat-form" class="composer" autocomplete="off">
162
- <textarea id="chat-input" placeholder="Type your prompt… e.g., Reply with exactly one word: pong."></textarea>
163
  <button id="chat-send" type="submit">Send</button>
164
  </form>
165
 
166
  <div class="hint">
167
- Tip: Backend endpoints are /chat/ready and /chat/run-agent on the same origin/port.
168
  </div>
169
  </div>
170
  </div>
 
122
  }
123
  a { color: var(--accent); text-decoration: none; }
124
  a:hover { text-decoration: underline; }
125
+
126
  /* Copy button styles */
127
  .code-wrapper {
128
  position: relative;
 
146
  background: #334155;
147
  border-color: #475569;
148
  }
149
+
150
+ /* Command execution styles */
151
+ .command-wrapper {
152
+ margin: 10px 0;
153
+ background: #0b1220;
154
+ border: 1px solid var(--border);
155
+ border-radius: 8px;
156
+ padding: 12px;
157
+ }
158
+ .command-block {
159
+ background: #1e293b;
160
+ padding: 12px;
161
+ border-radius: 6px;
162
+ margin: 0 0 8px 0;
163
+ font-family: 'Courier New', monospace;
164
+ color: #22c55e;
165
+ border-left: 3px solid #22c55e;
166
+ }
167
+ .command-buttons {
168
+ display: flex;
169
+ gap: 8px;
170
+ }
171
+ .execute-btn {
172
+ padding: 6px 16px;
173
+ font-size: 13px;
174
+ background: linear-gradient(180deg, #22c55e, #16a34a);
175
+ color: white;
176
+ border: 1px solid #16a34a;
177
+ border-radius: 6px;
178
+ cursor: pointer;
179
+ font-weight: 600;
180
+ transition: all 0.2s;
181
+ }
182
+ .execute-btn:hover:not(:disabled) {
183
+ background: linear-gradient(180deg, #16a34a, #15803d);
184
+ transform: translateY(-1px);
185
+ }
186
+ .execute-btn:disabled {
187
+ opacity: 0.6;
188
+ cursor: not-allowed;
189
+ }
190
  </style>
191
  </head>
192
  <body>
 
200
  <div id="chat-output" class="panel"></div>
201
 
202
  <form id="chat-form" class="composer" autocomplete="off">
203
+ <textarea id="chat-input" placeholder="Type your prompt… e.g., Show me all running Docker containers."></textarea>
204
  <button id="chat-send" type="submit">Send</button>
205
  </form>
206
 
207
  <div class="hint">
208
+ Tip: Commands will appear with Execute buttons. Click to run them safely.
209
  </div>
210
  </div>
211
  </div>