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

feat: Remove execute button feature, add copy buttons for free version

Browse files
app.js CHANGED
@@ -55,7 +55,7 @@
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",
@@ -64,7 +64,7 @@
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);
@@ -80,7 +80,7 @@
80
  btn.textContent = "✗ Error";
81
  btn.style.background = "#ef4444";
82
  }
83
-
84
  setTimeout(() => {
85
  btn.disabled = false;
86
  btn.textContent = "Execute";
@@ -94,102 +94,98 @@
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);
188
  container.scrollTop = container.scrollHeight;
189
 
 
190
  if (window.Prism) {
191
  try {
192
- Prism.highlightAllUnder(container);
193
  } catch (_) {}
194
  }
195
 
@@ -201,9 +197,9 @@
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
 
@@ -247,7 +243,7 @@
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
@@ -256,10 +252,10 @@
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);
 
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",
 
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);
 
80
  btn.textContent = "✗ Error";
81
  btn.style.background = "#ef4444";
82
  }
83
+
84
  setTimeout(() => {
85
  btn.disabled = false;
86
  btn.textContent = "Execute";
 
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
+ // NEW: Add copy buttons to all code blocks in a container
106
+ function addCopyButtonsToCodeBlocks(container) {
107
+ // Find all <pre> elements that don't already have a copy button
108
+ container.querySelectorAll("pre").forEach((pre) => {
109
+ // Skip if already has a copy button wrapper
110
+ if (pre.parentElement && pre.parentElement.classList.contains("code-wrapper")) {
111
+ return;
112
+ }
113
+
114
+ // Get the code content
115
+ const codeEl = pre.querySelector("code");
116
+ const code = codeEl ? codeEl.textContent : pre.textContent;
117
+
118
+ if (!code || !code.trim()) return;
119
+
120
+ // Create wrapper
121
+ const wrapper = document.createElement("div");
122
+ wrapper.className = "code-wrapper";
123
+
124
+ // Create copy button
125
+ const copyBtn = createCopyButton(code);
126
+
127
+ // Insert wrapper before pre
128
+ pre.parentNode.insertBefore(wrapper, pre);
129
+
130
+ // Move pre into wrapper and add button
131
+ wrapper.appendChild(copyBtn);
132
+ wrapper.appendChild(pre);
133
+ });
134
+ }
135
+
136
  function appendMessage(container, sender, message, useMarkdown = false) {
137
  const msgDiv = document.createElement("div");
138
  msgDiv.classList.add("message");
139
 
140
  // Check for execute tags
141
  const commands = extractExecuteTags(message);
142
+
143
  if (commands.length > 0) {
144
  // Remove execute tags from display
145
  let displayText = message.replace(/<execute>.*?<\/execute>/gs, "");
146
+
147
  msgDiv.innerHTML = `<strong>${sender}:</strong>${useMarkdown ? renderMarkdown(displayText) : `<pre>${escapeHtml(displayText)}</pre>`}`;
148
+
149
  // Add execute buttons for each command
150
  commands.forEach(cmd => {
151
  const cmdWrapper = document.createElement("div");
152
  cmdWrapper.className = "command-wrapper";
153
+
154
  const cmdPre = document.createElement("pre");
155
  cmdPre.className = "command-block";
156
  cmdPre.textContent = cmd;
157
+
158
  const btnContainer = document.createElement("div");
159
  btnContainer.className = "command-buttons";
160
+
161
  const executeBtn = createExecuteButton(cmd, container);
162
  const copyBtn = createCopyButton(cmd);
163
+
164
  btnContainer.appendChild(executeBtn);
165
  btnContainer.appendChild(copyBtn);
166
+
167
  cmdWrapper.appendChild(cmdPre);
168
  cmdWrapper.appendChild(btnContainer);
169
  msgDiv.appendChild(cmdWrapper);
170
  });
171
+ } else if (useMarkdown) {
172
+ // Render markdown first
173
+ msgDiv.innerHTML = `<strong>${sender}:</strong><div class="markdown-content">${renderMarkdown(String(message))}</div>`;
174
+
175
+ // Add copy buttons to all code blocks
176
+ addCopyButtonsToCodeBlocks(msgDiv);
177
  } else {
178
+ // Plain text message
179
+ msgDiv.innerHTML = `<strong>${sender}:</strong> <pre>${escapeHtml(String(message))}</pre>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
 
182
  container.appendChild(msgDiv);
183
  container.scrollTop = container.scrollHeight;
184
 
185
+ // Syntax highlighting
186
  if (window.Prism) {
187
  try {
188
+ Prism.highlightAllUnder(msgDiv);
189
  } catch (_) {}
190
  }
191
 
 
197
  const msgDiv = document.createElement("div");
198
  msgDiv.classList.add("message");
199
  msgDiv.innerHTML = `<strong>${sender}:</strong> <span class="streaming-content"></span>`;
200
+
201
  const contentSpan = msgDiv.querySelector(".streaming-content");
202
+
203
  container.appendChild(msgDiv);
204
  container.scrollTop = container.scrollHeight;
205
 
 
243
  if (done) break;
244
 
245
  buffer += decoder.decode(value, { stream: true });
246
+
247
  // SSE format: "data: {json}\n\n"
248
  const lines = buffer.split("\n\n");
249
  buffer = lines.pop(); // Keep incomplete data in buffer
 
252
  if (!line.trim() || !line.startsWith("data: ")) continue;
253
 
254
  const jsonStr = line.substring(6); // Remove "data: " prefix
255
+
256
  try {
257
  const data = JSON.parse(jsonStr);
258
+
259
  if (data.type === "token") {
260
  fullText += data.text;
261
  streamUI.updateContent(fullText);
deepshell-backend/deepshell/__main__.py CHANGED
@@ -10,6 +10,7 @@ import json
10
  import subprocess
11
  import shlex
12
  from typing import Any, List
 
13
 
14
  from .llm import get_global_client
15
 
@@ -37,12 +38,56 @@ app.add_middleware(
37
 
38
  app.mount("/static", StaticFiles(directory=str(STATIC_ROOT), html=False), name="static")
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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():
@@ -67,6 +112,10 @@ def get_css():
67
  def favicon():
68
  return {"ok": True}
69
 
 
 
 
 
70
  @app.get("/chat/ready")
71
  def chat_ready():
72
  try:
@@ -75,18 +124,48 @@ def chat_ready():
75
  except Exception as e:
76
  return {"status": "error", "detail": str(e)}
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  @app.post("/chat/run-agent")
79
  def run_agent(req: ChatRequest):
80
  prompt = (req.prompt or "").strip()
81
  if not prompt:
82
  return {"error": "prompt is required"}
83
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  client = get_global_client(os.getenv("PROVIDER", "groq"))
85
- resp = client.chat(prompt, max_tokens=1024, temperature=0.2)
86
  try:
87
  text = resp.choices[0].message.content
88
  except Exception:
89
  text = str(resp)
 
 
 
 
90
  return {"answer": text}
91
 
92
  # ============================================
@@ -111,24 +190,24 @@ DANGEROUS_PATTERNS = [
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)}"
@@ -144,7 +223,7 @@ def execute_command_safe(command: str, timeout: int = 30) -> dict:
144
  timeout=timeout,
145
  cwd=os.getcwd(),
146
  )
147
-
148
  return {
149
  "success": True,
150
  "stdout": result.stdout,
@@ -181,7 +260,7 @@ def execute_command(req: ExecuteRequest):
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:
@@ -189,9 +268,14 @@ def execute_command(req: ExecuteRequest):
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
  # ============================================
@@ -207,7 +291,7 @@ def _extract_text_from_chunk(chunk: Any) -> str:
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]
@@ -215,7 +299,7 @@ def _extract_text_from_chunk(chunk: Any) -> str:
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 ""
@@ -226,46 +310,67 @@ async def _sse_generator(prompt: str):
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
 
@@ -274,7 +379,7 @@ 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",
 
10
  import subprocess
11
  import shlex
12
  from typing import Any, List
13
+ from datetime import datetime
14
 
15
  from .llm import get_global_client
16
 
 
38
 
39
  app.mount("/static", StaticFiles(directory=str(STATIC_ROOT), html=False), name="static")
40
 
41
+ # ============================================
42
+ # In-Memory Session History
43
+ # ============================================
44
+
45
+ session_history = [] # Our "in-memory database"
46
+
47
+ def add_to_history(role: str, content: str):
48
+ """Add a message to session history."""
49
+ session_history.append({
50
+ "role": role,
51
+ "content": content,
52
+ "timestamp": datetime.now().isoformat()
53
+ })
54
+
55
+ def get_context(max_messages: int = 10) -> str:
56
+ """Build context from last N messages for LLM."""
57
+ if not session_history:
58
+ return ""
59
+
60
+ # Get last N messages
61
+ recent = session_history[-max_messages:]
62
+
63
+ # Format as conversation
64
+ context_lines = []
65
+ for msg in recent:
66
+ role = msg["role"].capitalize()
67
+ content = msg["content"]
68
+ context_lines.append(f"{role}: {content}")
69
+
70
+ return "\n".join(context_lines)
71
+
72
+ def clear_history():
73
+ """Clear session history."""
74
+ global session_history
75
+ session_history = []
76
+
77
+ # ============================================
78
+ # Pydantic Models
79
+ # ============================================
80
+
81
  class ChatRequest(BaseModel):
82
  prompt: str
83
 
84
  class ExecuteRequest(BaseModel):
85
  command: str
86
 
87
+ # ============================================
88
+ # Static File Routes
89
+ # ============================================
90
+
91
  @app.get("/")
92
  def root_page():
93
  if INDEX_PATH.exists():
 
112
  def favicon():
113
  return {"ok": True}
114
 
115
+ # ============================================
116
+ # Chat Endpoints
117
+ # ============================================
118
+
119
  @app.get("/chat/ready")
120
  def chat_ready():
121
  try:
 
124
  except Exception as e:
125
  return {"status": "error", "detail": str(e)}
126
 
127
+ @app.get("/chat/history")
128
+ def get_history():
129
+ """Get current session history."""
130
+ return {
131
+ "history": session_history,
132
+ "count": len(session_history)
133
+ }
134
+
135
+ @app.post("/chat/clear")
136
+ def clear_chat():
137
+ """Clear session history."""
138
+ clear_history()
139
+ return {"status": "ok", "message": "History cleared"}
140
+
141
  @app.post("/chat/run-agent")
142
  def run_agent(req: ChatRequest):
143
  prompt = (req.prompt or "").strip()
144
  if not prompt:
145
  return {"error": "prompt is required"}
146
 
147
+ # Add user message to history
148
+ add_to_history("user", prompt)
149
+
150
+ # Build context
151
+ context = get_context(max_messages=10)
152
+
153
+ # Create full prompt with context
154
+ if context:
155
+ full_prompt = f"Previous conversation:\n{context}\n\nUser: {prompt}"
156
+ else:
157
+ full_prompt = prompt
158
+
159
  client = get_global_client(os.getenv("PROVIDER", "groq"))
160
+ resp = client.chat(full_prompt, max_tokens=1024, temperature=0.2)
161
  try:
162
  text = resp.choices[0].message.content
163
  except Exception:
164
  text = str(resp)
165
+
166
+ # Add assistant response to history
167
+ add_to_history("assistant", text)
168
+
169
  return {"answer": text}
170
 
171
  # ============================================
 
190
  def is_command_safe(command: str) -> tuple[bool, str]:
191
  """Check if command is safe to execute."""
192
  cmd_lower = command.lower().strip()
193
+
194
  # Check for dangerous patterns
195
  for pattern in DANGEROUS_PATTERNS:
196
  if pattern.lower() in cmd_lower:
197
  return False, f"Dangerous pattern detected: {pattern}"
198
+
199
  # Extract first word (command name)
200
  try:
201
  parts = shlex.split(command)
202
  if not parts:
203
  return False, "Empty command"
204
+
205
  cmd_name = parts[0].split("/")[-1] # Handle /usr/bin/docker -> docker
206
+
207
  # Check if command is in whitelist
208
  if cmd_name not in ALLOWED_COMMANDS:
209
  return False, f"Command '{cmd_name}' is not in the allowed list"
210
+
211
  return True, "OK"
212
  except Exception as e:
213
  return False, f"Failed to parse command: {str(e)}"
 
223
  timeout=timeout,
224
  cwd=os.getcwd(),
225
  )
226
+
227
  return {
228
  "success": True,
229
  "stdout": result.stdout,
 
260
  {"success": False, "error": "Command is required"},
261
  status_code=400
262
  )
263
+
264
  # Safety check
265
  is_safe, reason = is_command_safe(command)
266
  if not is_safe:
 
268
  {"success": False, "error": f"Command blocked: {reason}"},
269
  status_code=403
270
  )
271
+
272
  # Execute
273
  result = execute_command_safe(command)
274
+
275
+ # Add command execution to history
276
+ if result["success"]:
277
+ add_to_history("system", f"Executed: {command}\nOutput: {result['stdout'][:200]}")
278
+
279
  return result
280
 
281
  # ============================================
 
291
  delta = chunk["choices"][0].get("delta", {})
292
  return delta.get("content", "") or ""
293
  return chunk.get("content", "") or chunk.get("text", "") or ""
294
+
295
  # Handle object responses (SDK objects)
296
  if hasattr(chunk, "choices") and chunk.choices:
297
  choice = chunk.choices[0]
 
299
  return choice.delta.content or ""
300
  if hasattr(choice, "message") and hasattr(choice.message, "content"):
301
  return choice.message.content or ""
302
+
303
  return ""
304
  except Exception:
305
  return ""
 
310
  """
311
  # Send start event
312
  yield f"data: {json.dumps({'type': 'start'})}\n\n"
313
+
314
+ # Add user message to history
315
+ add_to_history("user", prompt)
316
+
317
+ # Build context from history
318
+ context = get_context(max_messages=10)
319
 
320
+ # Create full prompt with context
321
+ if context:
322
+ full_prompt = f"Previous conversation:\n{context}\n\nUser: {prompt}"
323
+ else:
324
+ full_prompt = prompt
325
+
326
  try:
327
  client = get_global_client(os.getenv("PROVIDER", "groq"))
328
  except Exception as e:
329
  yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
330
  yield f"data: {json.dumps({'type': 'done'})}\n\n"
331
  return
332
+
333
+ full_response = ""
334
+
335
  try:
336
  # Try streaming with stream=True
337
  try:
338
+ response = client.chat(full_prompt, max_tokens=1024, temperature=0.2, stream=True)
339
+
340
  # Handle streaming response
341
  if hasattr(response, "__iter__"):
342
  for chunk in response:
343
  text = _extract_text_from_chunk(chunk)
344
  if text:
345
+ full_response += text
346
  yield f"data: {json.dumps({'type': 'token', 'text': text})}\n\n"
347
  await asyncio.sleep(0) # Yield control to event loop
348
  else:
349
  # Not iterable, treat as single response
350
  text = _extract_text_from_chunk(response)
351
  if text:
352
+ full_response = text
353
  yield f"data: {json.dumps({'type': 'token', 'text': text})}\n\n"
354
+
355
  except TypeError:
356
  # stream=True not supported, fallback to non-streaming
357
+ response = client.chat(full_prompt, max_tokens=1024, temperature=0.2)
358
  try:
359
  text = response.choices[0].message.content
360
  except Exception:
361
  text = str(response)
362
+
363
  if text:
364
+ full_response = text
365
  yield f"data: {json.dumps({'type': 'token', 'text': text})}\n\n"
366
+
367
  except Exception as e:
368
  yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
369
+
370
+ # Add assistant response to history
371
+ if full_response:
372
+ add_to_history("assistant", full_response)
373
+
374
  # Send done event
375
  yield f"data: {json.dumps({'type': 'done'})}\n\n"
376
 
 
379
  prompt = (req.prompt or "").strip()
380
  if not prompt:
381
  return JSONResponse({"error": "prompt is required"}, status_code=400)
382
+
383
  return StreamingResponse(
384
  _sse_generator(prompt),
385
  media_type="text/event-stream",
deepshell-backend/deepshell/llm.py CHANGED
@@ -6,24 +6,26 @@ 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()
@@ -41,26 +43,26 @@ def get_global_client(provider: str = "groq") -> Any:
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,
 
6
 
7
  _singleton = {"client": None, "provider": None}
8
 
9
+ DEEPSHELL_SYSTEM_PROMPT = """You are DeepShell, an AI assistant specialized in Docker, Kubernetes, and system administration tasks.
10
 
11
+ Your role:
12
+ - Provide clear, accurate commands and scripts
13
+ - Explain what each command does
14
+ - Format code in markdown code blocks with proper language tags
15
+ - Be concise but thorough
16
+ - Prioritize security and best practices
17
 
18
+ When providing commands or scripts:
19
+ - Use proper markdown code blocks: ```bash or ```yaml or ```python
20
+ - Include comments explaining complex parts
21
+ - Provide full working examples (no placeholders)
22
+ - For multi-step tasks, show all steps clearly
 
23
 
24
  Example responses:
25
+ - "To see running containers, use:\n```bash\ndocker ps\n```"
26
+ - "Here's an Ansible playbook for Jenkins:\n```yaml\n---\n- hosts: localhost\n tasks:\n - name: Install Java\n apt:\n name: openjdk-11-jdk\n state: present\n```"
 
27
 
28
+ Be helpful, professional, and always prioritize user safety."""
29
 
30
  def get_global_client(provider: str = "groq") -> Any:
31
  provider = (provider or "groq").lower()
 
43
 
44
  class Wrapper:
45
  def chat(
46
+ self,
47
+ prompt: str,
48
+ max_tokens: int = 1024,
49
  temperature: float = 0.2,
50
  stream: bool = False,
51
  system_prompt: Optional[str] = None
52
  ):
53
  model = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
54
+
55
  messages = []
56
+
57
  # Add system prompt if provided, otherwise use default
58
  if system_prompt is not None:
59
  if system_prompt: # Only add if not empty string
60
  messages.append({"role": "system", "content": system_prompt})
61
  else:
62
  messages.append({"role": "system", "content": DEEPSHELL_SYSTEM_PROMPT})
63
+
64
  messages.append({"role": "user", "content": prompt})
65
+
66
  response = client.chat.completions.create(
67
  model=model,
68
  messages=messages,