muralipala1504 commited on
Commit
8574870
·
1 Parent(s): ade4144

feat: wire Groq and serve UI from backend

Browse files
app.js CHANGED
@@ -1,228 +1,129 @@
1
- // =====================
2
- // Utilities
3
- // =====================
4
- function escapeHtml(text) {
5
- return String(text).replace(/[&<>"']/g, (m) => ({
6
- '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
7
- })[m]);
8
- }
9
- function renderMarkdown(md) {
10
- if (window.marked) return marked.parse(md);
11
- return `<pre>${escapeHtml(md)}</pre>`;
12
- }
13
- function detectServiceFromPaste(paste) {
14
- const p = (paste || '').toLowerCase();
15
- if (p.includes('nginx')) return 'nginx';
16
- if (p.includes('sshd') || p.includes('ssh')) return 'sshd';
17
- if (p.includes('docker')) return 'docker';
18
- if (p.includes('systemd') || p.includes('unit')) return 'systemd';
19
- if (p.includes('postgres') || p.includes('psql')) return 'postgresql';
20
- if (p.includes('httpd') || p.includes('apache')) return 'apache';
21
- return 'auto';
22
- }
23
- function humanSize(bytes) {
24
- if (bytes < 1024) return `${bytes} B`;
25
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
26
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
27
- }
28
-
29
- // =====================
30
- // Chat (default tab)
31
- // =====================
32
- document.addEventListener('DOMContentLoaded', () => {
33
- const form = document.getElementById('chat-form');
34
- const input = document.getElementById('chat-input');
35
- const output = document.getElementById('chat-output');
36
- if (!form || !input || !output) return;
37
-
38
- form.addEventListener('submit', async (e) => {
39
- e.preventDefault();
40
- const command = input.value.trim();
41
- if (!command) return;
42
-
43
- appendMessage('User', command);
44
- input.value = '';
45
-
46
- try {
47
- const response = await fetch('/run-agent', {
48
- method: 'POST',
49
- headers: { 'Content-Type': 'application/json' },
50
- body: JSON.stringify({ prompt: command }),
51
- });
52
-
53
- if (!response.ok) {
54
- appendMessage('Error', `Server error: ${response.status} ${response.statusText}`);
55
- return;
56
- }
57
 
58
- const data = await response.json();
59
- appendMessage('Deepshell', data.output || 'No response', true);
60
- Prism.highlightAll();
61
- } catch (err) {
62
- appendMessage('Error', `Network error: ${err.message}`);
63
- }
64
- });
65
 
66
- function appendMessage(sender, message, tryMarkdown = false) {
67
- const msgDiv = document.createElement('div');
68
- msgDiv.classList.add('message');
69
 
70
- const codeBlockMatch = typeof message === 'string'
71
- ? message.match(/```(\w+)?\n([\s\S]*?)```/)
72
- : null;
 
73
 
74
  if (codeBlockMatch) {
75
- const lang = codeBlockMatch[1] || 'bash';
76
  const code = codeBlockMatch[2];
77
  msgDiv.innerHTML = `
78
  <strong>${sender}:</strong>
79
  <pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>
80
  `;
81
- } else if (tryMarkdown) {
82
  msgDiv.innerHTML = `<strong>${sender}:</strong>${renderMarkdown(String(message))}`;
83
  } else {
84
  msgDiv.innerHTML = `<strong>${sender}:</strong> <pre>${escapeHtml(String(message))}</pre>`;
85
  }
86
 
87
- output.appendChild(msgDiv);
88
- output.scrollTop = output.scrollHeight;
89
- }
90
- });
91
-
92
- // =====================
93
- // Tabs
94
- // =====================
95
- document.addEventListener('DOMContentLoaded', () => {
96
- const tabs = document.querySelectorAll('.tab');
97
- const panels = document.querySelectorAll('.panel');
98
- if (!tabs.length || !panels.length) return;
99
-
100
- tabs.forEach(tab => {
101
- tab.addEventListener('click', () => {
102
- tabs.forEach(t => t.classList.remove('active'));
103
- panels.forEach(p => p.classList.remove('active'));
104
- tab.classList.add('active');
105
- document.getElementById(tab.dataset.target).classList.add('active');
106
- });
107
- });
108
- });
109
-
110
- // =====================
111
- // Clinic
112
- // =====================
113
- document.addEventListener('DOMContentLoaded', () => {
114
- const runBtn = document.getElementById('clinic-run');
115
- const out = document.getElementById('clinic-output');
116
- if (!runBtn || !out) return;
117
-
118
- runBtn.addEventListener('click', async () => {
119
- const paste = document.getElementById('clinic-input').value.trim();
120
- const osName = document.getElementById('clinic-os').value;
121
- let service = document.getElementById('clinic-service').value;
122
- const context = document.getElementById('clinic-context').value;
123
- const filesInput = document.getElementById('clinic-files');
124
- const files = filesInput ? filesInput.files : [];
125
-
126
- if (service === 'auto' && paste) service = detectServiceFromPaste(paste);
127
-
128
- if (!paste && (!files || files.length === 0)) {
129
- out.innerHTML = '<div class="message"><strong>Clinic:</strong> <pre>Please paste logs or attach files.</pre></div>';
130
- return;
131
- }
132
 
133
- const formData = new FormData();
134
- formData.append('paste', paste);
135
- formData.append('os_name', osName);
136
- formData.append('service', service);
137
- formData.append('context', context);
138
- for (let i = 0; i < files.length; i++) formData.append('files', files[i]);
139
-
140
- out.innerHTML = '<div class="message"><strong>Clinic:</strong> <pre>Analyzing…</pre></div>';
141
-
142
- try {
143
- const res = await fetch('/clinic/diagnose', { method: 'POST', body: formData });
144
- let data;
145
- try { data = await res.json(); } catch {
146
- out.innerHTML = `<div class="message"><strong>Error:</strong> <pre>Bad JSON response (${res.status} ${res.statusText})</pre></div>`;
147
- return;
148
  }
 
 
149
 
150
- if (!res.ok || data.success === false) {
151
- const errText = (data && (data.error || data.detail)) || `${res.status} ${res.statusText}`;
152
- out.innerHTML = `<div class="message"><strong>Error:</strong> <pre>${escapeHtml(errText)}</pre></div>`;
153
- return;
154
- }
 
155
 
156
- const message = data.markdown || data.output || 'No response';
157
- out.innerHTML = `<div class="message"><strong>Clinic:</strong>${renderMarkdown(message)}</div>`;
158
- Prism.highlightAll();
159
- } catch (e) {
160
- out.innerHTML = `<div class="message"><strong>Error:</strong> <pre>${escapeHtml(e.message)}</pre></div>`;
161
  }
162
- });
163
- });
164
-
165
- // =====================
166
- // Cases
167
- // =====================
168
- document.addEventListener('DOMContentLoaded', () => {
169
- const listEl = document.getElementById('cases-list');
170
- const refreshBtn = document.getElementById('cases-refresh');
171
- const viewer = document.getElementById('case-viewer');
172
- const titleEl = document.getElementById('case-title');
173
- const contentEl = document.getElementById('case-content');
174
-
175
- if (!listEl || !refreshBtn) return;
176
-
177
- async function loadCases() {
178
- listEl.innerHTML = '<div class="message"><strong>Cases:</strong> Loading…</div>';
179
- try {
180
- const res = await fetch('/clinic/cases');
181
- const data = await res.json();
182
- const cases = (data && data.cases) || [];
183
- if (!cases.length) {
184
- listEl.innerHTML = '<div class="message"><strong>Cases:</strong> No cases yet.</div>';
185
- return;
186
- }
187
- listEl.innerHTML = '';
188
- cases.forEach(c => {
189
- const row = document.createElement('div');
190
- row.className = 'case-row';
191
- row.innerHTML = `
192
- <div><strong>${c.id}</strong> <span style="color:#6b7280;">(${humanSize(c.size)})</span></div>
193
- <div class="case-actions">
194
- <button class="btn btn-view" data-id="${c.id}">View</button>
195
- <a class="btn" href="/clinic/cases/${c.id}" download="${c.id}.md">Download</a>
196
- </div>
197
- `;
198
- listEl.appendChild(row);
199
- });
200
 
201
- // Wire view buttons
202
- listEl.querySelectorAll('.btn-view').forEach(btn => {
203
- btn.addEventListener('click', async () => {
204
- const id = btn.getAttribute('data-id');
205
- titleEl.textContent = `Case ${id}`;
206
- viewer.style.display = 'block';
207
- contentEl.innerHTML = '<div class="message">Loading…</div>';
208
- const res = await fetch(`/clinic/cases/${id}`);
209
- if (!res.ok) {
210
- contentEl.innerHTML = `<div class="message"><strong>Error:</strong> <pre>${res.status} ${res.statusText}</pre></div>`;
 
 
 
 
 
 
 
211
  return;
212
  }
213
- const md = await res.text();
214
- contentEl.innerHTML = renderMarkdown(md);
215
- Prism.highlightAll();
216
- });
217
- });
218
- } catch (e) {
219
- listEl.innerHTML = `<div class="message"><strong>Error:</strong> <pre>${escapeHtml(e.message)}</pre></div>`;
 
 
 
 
 
 
 
 
 
 
 
220
  }
221
- }
222
 
223
- refreshBtn.addEventListener('click', loadCases);
 
 
 
 
 
 
 
 
 
 
224
 
225
- // Auto-load when tab becomes active
226
- const casesTab = document.querySelector('.tab[data-target="cases"]');
227
- casesTab.addEventListener('click', loadCases);
228
- });
 
 
 
 
 
1
+ // Plain Vanilla Chat Frontend
2
+ // - One textarea, one Send button, one output area
3
+ // - POSTs to /chat/run-agent with { prompt }
4
+ // - Renders answer or error; optional Markdown if window.marked is available
5
+
6
+ (function () {
7
+ "use strict";
8
+
9
+ // ---------- Utilities ----------
10
+ function escapeHtml(text) {
11
+ return String(text).replace(/[&<>"']/g, (m) => ({
12
+ "&": "&amp;",
13
+ "<": "&lt;",
14
+ ">": "&gt;",
15
+ '"': "&quot;",
16
+ "'": "&#39;",
17
+ })[m]);
18
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ function renderMarkdown(md) {
21
+ if (window.marked) return marked.parse(md);
22
+ return `<pre>${escapeHtml(md)}</pre>`;
23
+ }
 
 
 
24
 
25
+ function appendMessage(container, sender, message, useMarkdown = false) {
26
+ const msgDiv = document.createElement("div");
27
+ msgDiv.classList.add("message");
28
 
29
+ const codeBlockMatch =
30
+ typeof message === "string"
31
+ ? message.match(/```(\w+)?\n([\s\S]*?)```/)
32
+ : null;
33
 
34
  if (codeBlockMatch) {
35
+ const lang = codeBlockMatch[1] || "bash";
36
  const code = codeBlockMatch[2];
37
  msgDiv.innerHTML = `
38
  <strong>${sender}:</strong>
39
  <pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>
40
  `;
41
+ } else if (useMarkdown) {
42
  msgDiv.innerHTML = `<strong>${sender}:</strong>${renderMarkdown(String(message))}`;
43
  } else {
44
  msgDiv.innerHTML = `<strong>${sender}:</strong> <pre>${escapeHtml(String(message))}</pre>`;
45
  }
46
 
47
+ container.appendChild(msgDiv);
48
+ container.scrollTop = container.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
+ if (window.Prism) {
51
+ try {
52
+ Prism.highlightAllUnder(container);
53
+ } catch (_) {
54
+ // no-op if Prism not available
 
 
 
 
 
 
 
 
 
 
55
  }
56
+ }
57
+ }
58
 
59
+ // ---------- Chat wiring ----------
60
+ document.addEventListener("DOMContentLoaded", () => {
61
+ const form = document.getElementById("chat-form");
62
+ const input = document.getElementById("chat-input");
63
+ const output = document.getElementById("chat-output");
64
+ const sendBtn = document.getElementById("chat-send");
65
 
66
+ if (!form || !input || !output) {
67
+ console.warn("Chat elements not found (chat-form/chat-input/chat-output).");
68
+ return;
 
 
69
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ async function sendPrompt(prompt) {
72
+ try {
73
+ const response = await fetch("/chat/run-agent", {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify({ prompt }),
77
+ });
78
+
79
+ // Try JSON; if it fails, show raw text to help debugging
80
+ let data = null;
81
+ let rawText = "";
82
+ try {
83
+ rawText = await response.clone().text();
84
+ data = JSON.parse(rawText);
85
+ } catch {
86
+ if (!response.ok) {
87
+ appendMessage(output, "Error", `Server error: ${response.status} ${response.statusText}\n${rawText || "(no body)"}`);
88
  return;
89
  }
90
+ appendMessage(output, "Error", `Bad JSON response (${response.status} ${response.statusText})`);
91
+ return;
92
+ }
93
+
94
+ if (!response.ok || data.error) {
95
+ const errText = data.error || `${response.status} ${response.statusText}`;
96
+ appendMessage(output, "Error", errText);
97
+ return;
98
+ }
99
+
100
+ const text = data.answer ?? data.output ?? "No response";
101
+ appendMessage(output, "DeepShell", text, true);
102
+ } catch (err) {
103
+ appendMessage(output, "Error", `Network error: ${err.message}`);
104
+ } finally {
105
+ if (sendBtn) sendBtn.disabled = false;
106
+ input.focus();
107
+ }
108
  }
 
109
 
110
+ form.addEventListener("submit", (e) => {
111
+ e.preventDefault();
112
+ const prompt = input.value.trim();
113
+ if (!prompt) return;
114
+
115
+ appendMessage(output, "You", prompt);
116
+ input.value = "";
117
+ if (sendBtn) sendBtn.disabled = true;
118
+
119
+ sendPrompt(prompt);
120
+ });
121
 
122
+ if (sendBtn) {
123
+ sendBtn.addEventListener("click", (e) => {
124
+ e.preventDefault();
125
+ form.requestSubmit();
126
+ });
127
+ }
128
+ });
129
+ })();
deepshell-backend/deepshell/__main__.py CHANGED
@@ -1,12 +1,105 @@
 
 
 
 
 
 
 
1
 
2
- """
3
- Entry point for running DeepShell as a module.
4
 
5
- Usage:
6
- python -m deepshell [OPTIONS] [PROMPT]
7
- """
 
 
 
8
 
9
- from .cli import app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  if __name__ == "__main__":
12
- app()
 
 
 
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
 
10
 
11
+ # Resolve paths
12
+ # This file is at: deepshell-backend/deepshell/__main__.py
13
+ # Repo root is two levels up from here
14
+ REPO_ROOT = Path(__file__).resolve().parents[2] # deepshell_modui/
15
+ INDEX_PATH = REPO_ROOT / "index.html"
16
+ STATIC_ROOT = REPO_ROOT # serve app.js, services.css, etc. from repo root
17
 
18
+ app = FastAPI(title="DeepShell Backend + UI")
19
+
20
+ # CORS (kept minimal; mostly redundant when same-origin)
21
+ default_origins = [
22
+ "http://localhost",
23
+ "http://localhost:8001",
24
+ "http://127.0.0.1",
25
+ "http://127.0.0.1:8001",
26
+ ]
27
+ env_origins = os.getenv("CORS_ORIGINS", "")
28
+ extra_origins = [o.strip() for o in env_origins.split(",") if o.strip()]
29
+ allowed_origins = default_origins + extra_origins
30
+
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=allowed_origins,
34
+ allow_credentials=True,
35
+ allow_methods=["GET", "POST", "OPTIONS"],
36
+ allow_headers=["*"],
37
+ )
38
+
39
+ # Mount static files from repo root at /static (and also expose top-level files)
40
+ # We’ll explicitly serve index.html at /, and expose other files at their names.
41
+ # Primary mount so /app.js, /services.css, etc. work:
42
+ app.mount("/static", StaticFiles(directory=str(STATIC_ROOT), html=False), name="static")
43
+
44
+ class ChatRequest(BaseModel):
45
+ prompt: str
46
+
47
+ # Serve index.html at /
48
+ @app.get("/")
49
+ def root_page():
50
+ if INDEX_PATH.exists():
51
+ return FileResponse(str(INDEX_PATH))
52
+ return {
53
+ "name": "DeepShell Backend",
54
+ "status": "ok",
55
+ "endpoints": ["/chat/ready", "/chat/run-agent"],
56
+ "note": f"index.html not found at {INDEX_PATH}",
57
+ }
58
+
59
+ # Convenience routes to serve common top-level assets directly
60
+ # so your <script src="./app.js"> works without /static prefix.
61
+ @app.get("/app.js")
62
+ def get_app_js():
63
+ path = STATIC_ROOT / "app.js"
64
+ if path.exists():
65
+ return FileResponse(str(path), media_type="application/javascript")
66
+ return {"detail": "Not Found"}, 404
67
+
68
+ @app.get("/services.css")
69
+ def get_css():
70
+ path = STATIC_ROOT / "services.css"
71
+ if path.exists():
72
+ return FileResponse(str(path), media_type="text/css")
73
+ return {"detail": "Not Found"}, 404
74
+
75
+ @app.get("/chat/ready")
76
+ def chat_ready():
77
+ try:
78
+ _ = get_global_client(os.getenv("PROVIDER", "groq"))
79
+ return {"status": "ok"}
80
+ except Exception as e:
81
+ return {"status": "error", "detail": str(e)}
82
+
83
+ @app.post("/chat/run-agent")
84
+ def run_agent(req: ChatRequest):
85
+ prompt = (req.prompt or "").strip()
86
+ if not prompt:
87
+ return {"error": "prompt is required"}
88
+
89
+ client = get_global_client(os.getenv("PROVIDER", "groq"))
90
+ resp = client.chat(prompt, max_tokens=1024, temperature=0.1)
91
+
92
+ if not hasattr(resp, "choices") or not resp.choices:
93
+ try:
94
+ text = resp.choices[0].message.content
95
+ except Exception:
96
+ text = str(resp)
97
+ return {"error": text}
98
+
99
+ text = resp.choices[0].message.content
100
+ return {"answer": text}
101
 
102
  if __name__ == "__main__":
103
+ import uvicorn
104
+ port = int(os.getenv("PORT", "8001"))
105
+ uvicorn.run("deepshell.__main__:app", host="0.0.0.0", port=port, reload=False)
deepshell-backend/deepshell/llm.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
- OpenAI LLM client integration using LiteLLM.
3
 
4
- This module provides a unified interface for interacting with OpenAI language models.
5
  """
6
 
7
  import json
@@ -12,6 +12,7 @@ from abc import ABC, abstractmethod
12
 
13
  import litellm
14
  from litellm import completion
 
15
  from rich.console import Console
16
 
17
  from .config import config
@@ -224,32 +225,109 @@ class OpenAIClient(BaseLLMClient):
224
  return "gpt-3.5-turbo"
225
 
226
 
227
- # Provider registry (OpenAI only)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  PROVIDERS = {
229
  "openai": OpenAIClient,
 
230
  }
231
 
232
 
233
  def get_client(provider: Optional[str] = None) -> BaseLLMClient:
234
- """Get LLM client for OpenAI."""
235
- provider_name = provider or config.get("PROVIDER", "openai")
236
- if provider_name != "openai":
237
- raise ValueError("Only 'openai' provider is supported in this version.")
238
  client_class = PROVIDERS[provider_name]
 
 
 
 
 
 
 
239
  return client_class(
 
240
  timeout=config.get("REQUEST_TIMEOUT", 60),
241
  max_retries=config.get("MAX_RETRIES", 3),
242
  )
243
 
244
 
245
  def get_available_providers() -> List[str]:
246
- """Get list of available providers (OpenAI only)."""
247
- return ["openai"]
248
 
249
 
250
  def validate_provider(provider: str) -> bool:
251
- """Validate if provider is available (OpenAI only)."""
252
- return provider == "openai"
253
 
254
 
255
  # Global client instance
@@ -260,9 +338,9 @@ _current_provider: Optional[str] = None
260
  def get_global_client(provider: Optional[str] = None) -> BaseLLMClient:
261
  """Get or create global client instance."""
262
  global _client, _current_provider
263
- provider_name = provider or config.get("PROVIDER", "openai")
264
- if provider_name != "openai":
265
- raise ValueError("Only 'openai' provider is supported in this version.")
266
  if _client is None or _current_provider != provider_name:
267
  _client = get_client(provider_name)
268
  _current_provider = provider_name
 
1
  """
2
+ OpenAI and Groq LLM client integration using LiteLLM and Groq SDK.
3
 
4
+ This module provides a unified interface for interacting with language models.
5
  """
6
 
7
  import json
 
12
 
13
  import litellm
14
  from litellm import completion
15
+ from groq import Groq
16
  from rich.console import Console
17
 
18
  from .config import config
 
225
  return "gpt-3.5-turbo"
226
 
227
 
228
+ class GroqClient(BaseLLMClient):
229
+ """Groq LLM client (OpenAI-compatible chat.completions)."""
230
+
231
+ def __init__(self, api_key: Optional[str] = None, **kwargs):
232
+ super().__init__(api_key, **kwargs)
233
+ # Prefer explicit key, then config, then env
234
+ self.api_key = api_key or config.get("GROQ_API_KEY") or os.getenv("GROQ_API_KEY")
235
+ if not self.api_key:
236
+ raise ValueError(
237
+ "Groq API key is required. Set GROQ_API_KEY in env or config."
238
+ )
239
+ # The Groq SDK handles its own base
240
+ self.client = Groq(api_key=self.api_key)
241
+ # Default model as per CLI tests
242
+ self.default_model = config.get("LLM_MODEL") or os.getenv("LLM_MODEL", "llama-3.3-70b-versatile")
243
+
244
+ def get_model_prefix(self) -> str:
245
+ return "groq/"
246
+
247
+ def get_available_models(self) -> List[str]:
248
+ return [
249
+ "llama-3.3-70b-versatile",
250
+ "llama-3.1-8b-instant",
251
+ ]
252
+
253
+ def get_default_model(self) -> str:
254
+ return self.default_model
255
+
256
+ def complete(
257
+ self,
258
+ messages: List[Dict[str, str]],
259
+ model: Optional[str] = None,
260
+ temperature: float = 0.0,
261
+ top_p: float = 1.0,
262
+ max_tokens: Optional[int] = None,
263
+ stream: bool = False,
264
+ functions: Optional[List[Dict[str, Any]]] = None,
265
+ **kwargs
266
+ ) -> Union[Any, Generator[str, None, None]]:
267
+ """
268
+ Override complete to call Groq SDK directly, keeping the same shape as BaseLLMClient expects.
269
+ """
270
+ model_name = model or self.get_default_model()
271
+ try:
272
+ if stream:
273
+ # Groq stream returns an iterator of chunks
274
+ response = self.client.chat.completions.create(
275
+ model=model_name,
276
+ messages=messages,
277
+ temperature=temperature,
278
+ max_tokens=max_tokens,
279
+ stream=True,
280
+ )
281
+ # Return the iterator directly; BaseLLMClient._stream_completion expects an iterator
282
+ return response
283
+ else:
284
+ response = self.client.chat.completions.create(
285
+ model=model_name,
286
+ messages=messages,
287
+ temperature=temperature,
288
+ max_tokens=max_tokens,
289
+ stream=False,
290
+ )
291
+ return response
292
+ except Exception as e:
293
+ return self._create_error_response(str(e))
294
+
295
+
296
+ # Provider registry (OpenAI and Groq)
297
  PROVIDERS = {
298
  "openai": OpenAIClient,
299
+ "groq": GroqClient,
300
  }
301
 
302
 
303
  def get_client(provider: Optional[str] = None) -> BaseLLMClient:
304
+ """Get LLM client (OpenAI or Groq)."""
305
+ provider_name = provider or config.get("PROVIDER") or os.getenv("PROVIDER", "groq")
306
+ if provider_name not in PROVIDERS:
307
+ raise ValueError(f"Unsupported provider '{provider_name}'. Use one of: {list(PROVIDERS.keys())}")
308
  client_class = PROVIDERS[provider_name]
309
+
310
+ # Determine which API key to use based on provider
311
+ if provider_name == "groq":
312
+ api_key = config.get("GROQ_API_KEY") or os.getenv("GROQ_API_KEY")
313
+ else:
314
+ api_key = config.get("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY")
315
+
316
  return client_class(
317
+ api_key=api_key,
318
  timeout=config.get("REQUEST_TIMEOUT", 60),
319
  max_retries=config.get("MAX_RETRIES", 3),
320
  )
321
 
322
 
323
  def get_available_providers() -> List[str]:
324
+ """Get list of available providers."""
325
+ return list(PROVIDERS.keys())
326
 
327
 
328
  def validate_provider(provider: str) -> bool:
329
+ """Validate if provider is available."""
330
+ return provider in PROVIDERS
331
 
332
 
333
  # Global client instance
 
338
  def get_global_client(provider: Optional[str] = None) -> BaseLLMClient:
339
  """Get or create global client instance."""
340
  global _client, _current_provider
341
+ provider_name = provider or config.get("PROVIDER") or os.getenv("PROVIDER", "groq")
342
+ if provider_name not in PROVIDERS:
343
+ raise ValueError(f"Unsupported provider '{provider_name}'. Use one of: {list(PROVIDERS.keys())}")
344
  if _client is None or _current_provider != provider_name:
345
  _client = get_client(provider_name)
346
  _current_provider = provider_name
deepshell-backend/requirements.txt CHANGED
@@ -1,3 +1,4 @@
 
1
  typer>=0.9.0
2
  rich>=13.0.0
3
  litellm==1.74.9.post1
 
1
+ groq>=0.9.0
2
  typer>=0.9.0
3
  rich>=13.0.0
4
  litellm==1.74.9.post1
index.html CHANGED
@@ -3,113 +3,161 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>Deepshell UI</title>
7
 
 
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" />
9
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
10
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
 
 
11
  <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.2/marked.min.js"></script>
12
 
13
  <style>
14
- body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:#fafafa; margin:0; }
15
- .container { max-width: 960px; margin: 24px auto; padding: 0 16px; }
16
- h1 { font-size: 22px; margin: 0 0 12px; }
17
- .tabs { display: flex; gap: 8px; margin-bottom: 12px; }
18
- .tab { background:#eee; border:1px solid #ddd; padding:8px 12px; border-radius:8px; cursor:pointer; }
19
- .tab.active { background:#2563eb; color:#fff; border-color:#2563eb; }
20
- .panel { display:none; background:#fff; border:1px solid #e5e7eb; border-radius: 10px; padding:14px; }
21
- .panel.active { display:block; }
22
-
23
- textarea { width:100%; min-height: 180px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 14px; }
24
- .message { background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:12px; margin:12px 0; white-space: normal; }
25
- pre { background:#0b1021; color:#e4e7f0; padding:10px; border-radius:8px; overflow:auto; }
26
- .controls { display:flex; gap:8px; align-items:center; margin: 8px 0; }
27
- .btn { background:#2563eb; color:#fff; border:none; border-radius:8px; padding:8px 12px; cursor:pointer; }
28
- .btn:disabled { opacity: 0.6; cursor: default; }
29
- select { padding:6px 8px; border-radius:8px; border:1px solid #e5e7eb; }
30
-
31
- .cases-list { border-top: 1px solid #e5e7eb; margin-top: 8px; }
32
- .case-row { display:flex; justify-content: space-between; align-items:center; padding:8px 0; border-bottom:1px solid #f0f0f0; font-family: ui-monospace, monospace; }
33
- .case-actions button { margin-left: 8px; }
34
- .case-viewer { margin-top: 12px; background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  </style>
36
  </head>
37
  <body>
38
  <div class="container">
39
- <h1>Deepshell UI</h1>
 
 
 
40
 
41
- <div class="tabs">
42
- <div class="tab active" data-target="chat">Chat</div>
43
- <div class="tab" data-target="clinic">Clinic</div>
44
- <div class="tab" data-target="cases">Cases</div>
45
- </div>
46
 
47
- <!-- Chat Panel -->
48
- <div id="chat" class="panel active">
49
- <form id="chat-form" class="controls">
50
- <input id="chat-input" type="text" placeholder="Describe your infra task..." style="flex:1; padding:8px; border:1px solid #e5e7eb; border-radius:8px;" />
51
- <button class="btn" type="submit">Run</button>
52
  </form>
53
- <div id="chat-output"></div>
54
- </div>
55
-
56
- <!-- Clinic Panel -->
57
- <div id="clinic" class="panel">
58
- <div class="controls" style="flex-wrap: wrap;">
59
- <label>OS:
60
- <select id="clinic-os">
61
- <option value="auto" selected>auto</option>
62
- <option value="rhel">rhel</option>
63
- <option value="ubuntu">ubuntu</option>
64
- <option value="debian">debian</option>
65
- </select>
66
- </label>
67
- <label>Service:
68
- <select id="clinic-service">
69
- <option value="auto" selected>auto</option>
70
- <option value="nginx">nginx</option>
71
- <option value="sshd">sshd</option>
72
- <option value="docker">docker</option>
73
- <option value="systemd">systemd</option>
74
- </select>
75
- </label>
76
- <label>Context:
77
- <select id="clinic-context">
78
- <option value="dev" selected>dev</option>
79
- <option value="prod">prod</option>
80
- </select>
81
- </label>
82
- </div>
83
 
84
- <div class="controls">
85
- <label>Attach files
86
- <input id="clinic-files" type="file" multiple />
87
- </label>
88
- </div>
89
-
90
- <textarea id="clinic-input" placeholder="Paste logs or config snippets here..."></textarea>
91
-
92
- <div class="controls">
93
- <button id="clinic-run" class="btn" type="button">Diagnose</button>
94
- <span style="color:#6b7280;">Deepshell Clinic analyzes the paste/files and returns diagnosis, fixes, and verification steps.</span>
95
- </div>
96
-
97
- <div id="clinic-output" style="margin-top:12px;"></div>
98
- </div>
99
-
100
- <!-- Cases Panel -->
101
- <div id="cases" class="panel">
102
- <div class="controls">
103
- <button id="cases-refresh" class="btn" type="button">Refresh</button>
104
- </div>
105
- <div id="cases-list" class="cases-list"></div>
106
- <div id="case-viewer" class="case-viewer" style="display:none;">
107
- <h3 id="case-title" style="margin-top:0;">Case</h3>
108
- <div id="case-content"></div>
109
  </div>
110
  </div>
111
  </div>
112
 
113
- <script src="/static/app.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  </body>
115
  </html>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>DeepShell Chat</title>
7
 
8
+ <!-- Optional: Prism for code highlighting inside responses -->
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" />
10
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
11
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
12
+
13
+ <!-- Optional: marked for Markdown rendering -->
14
  <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.2/marked.min.js"></script>
15
 
16
  <style>
17
+ :root {
18
+ --bg: #0b1220;
19
+ --panel: #0f172a;
20
+ --border: #1e293b;
21
+ --text: #e5e7eb;
22
+ --muted: #94a3b8;
23
+ --accent: #22c55e;
24
+ --accent-2: #3b82f6;
25
+ }
26
+ * { box-sizing: border-box; }
27
+ body {
28
+ margin: 0;
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
32
+ }
33
+ .container {
34
+ max-width: 900px;
35
+ margin: 24px auto;
36
+ padding: 0 16px;
37
+ }
38
+ header {
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: space-between;
42
+ margin-bottom: 16px;
43
+ }
44
+ .brand {
45
+ font-weight: 700;
46
+ font-size: 18px;
47
+ letter-spacing: 0.4px;
48
+ }
49
+ .status {
50
+ font-size: 12px;
51
+ color: var(--muted);
52
+ }
53
+ .panel {
54
+ background: var(--panel);
55
+ border: 1px solid var(--border);
56
+ border-radius: 10px;
57
+ padding: 12px;
58
+ }
59
+ #chat-output {
60
+ height: 55vh;
61
+ overflow-y: auto;
62
+ padding: 8px;
63
+ background: #0b1220;
64
+ border: 1px solid var(--border);
65
+ border-radius: 8px;
66
+ }
67
+ .message {
68
+ margin: 10px 0;
69
+ padding: 8px 10px;
70
+ background: #0b1220;
71
+ border: 1px solid var(--border);
72
+ border-radius: 8px;
73
+ line-height: 1.5;
74
+ }
75
+ .message strong {
76
+ display: inline-block;
77
+ color: var(--accent-2);
78
+ margin-bottom: 6px;
79
+ }
80
+ pre {
81
+ margin: 6px 0 0;
82
+ white-space: pre-wrap;
83
+ word-break: break-word;
84
+ }
85
+ .composer {
86
+ margin-top: 12px;
87
+ display: grid;
88
+ grid-template-columns: 1fr auto;
89
+ gap: 8px;
90
+ align-items: start;
91
+ }
92
+ #chat-input {
93
+ width: 100%;
94
+ resize: vertical;
95
+ min-height: 80px;
96
+ max-height: 240px;
97
+ padding: 10px 12px;
98
+ border-radius: 8px;
99
+ border: 1px solid var(--border);
100
+ background: #0b1220;
101
+ color: var(--text);
102
+ font-family: inherit;
103
+ font-size: 14px;
104
+ }
105
+ #chat-send {
106
+ height: 40px;
107
+ padding: 0 14px;
108
+ border: 1px solid var(--border);
109
+ background: linear-gradient(180deg, #1f2937, #111827);
110
+ color: var(--text);
111
+ border-radius: 8px;
112
+ cursor: pointer;
113
+ font-weight: 600;
114
+ }
115
+ #chat-send:hover {
116
+ border-color: #334155;
117
+ }
118
+ .hint {
119
+ margin-top: 8px;
120
+ color: var(--muted);
121
+ font-size: 12px;
122
+ }
123
+ a { color: var(--accent); text-decoration: none; }
124
+ a:hover { text-decoration: underline; }
125
  </style>
126
  </head>
127
  <body>
128
  <div class="container">
129
+ <header>
130
+ <div class="brand">DeepShell Chat</div>
131
+ <div class="status">Backend: <span id="status">checking…</span></div>
132
+ </header>
133
 
134
+ <div class="panel">
135
+ <div id="chat-output" class="panel"></div>
 
 
 
136
 
137
+ <form id="chat-form" class="composer" autocomplete="off">
138
+ <textarea id="chat-input" placeholder="Type your prompt… e.g., Reply with exactly one word: pong."></textarea>
139
+ <button id="chat-send" type="submit">Send</button>
 
 
140
  </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ <div class="hint">
143
+ Tip: Backend endpoints are /chat/ready and /chat/run-agent on the same origin/port.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  </div>
145
  </div>
146
  </div>
147
 
148
+ <script src="./app.js"></script>
149
+ <script>
150
+ // simple readiness check
151
+ (async () => {
152
+ const el = document.getElementById('status');
153
+ try {
154
+ const r = await fetch('/chat/ready');
155
+ const j = await r.json().catch(() => ({}));
156
+ el.textContent = (r.ok && j.status === 'ok') ? 'ok' : 'error';
157
+ } catch {
158
+ el.textContent = 'offline';
159
+ }
160
+ })();
161
+ </script>
162
  </body>
163
  </html>
run_deepshell.py CHANGED
@@ -4,8 +4,14 @@ import os
4
  import signal
5
 
6
  def start_backend():
7
- cmd = [sys.executable, os.path.join("deepshell-backend", "wrapper.py")]
8
- process = subprocess.Popen(cmd)
 
 
 
 
 
 
9
  print(f"DeepShell backend started with PID {process.pid}")
10
  return process
11
 
 
4
  import signal
5
 
6
  def start_backend():
7
+ env = os.environ.copy()
8
+ # Ensure backend path is on PYTHONPATH
9
+ backend_dir = os.path.join(os.getcwd(), "deepshell-backend")
10
+ env["PYTHONPATH"] = backend_dir + os.pathsep + env.get("PYTHONPATH", "")
11
+ # Respect env vars already set in shell (PROVIDER, GROQ_API_KEY, LLM_MODEL, PORT)
12
+ # Run the deepshell package (__main__.py) which exposes the FastAPI app
13
+ cmd = [sys.executable, "-m", "deepshell"]
14
+ process = subprocess.Popen(cmd, env=env, cwd=backend_dir)
15
  print(f"DeepShell backend started with PID {process.pid}")
16
  return process
17