Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Commit 7ee6794

Browse files
committed
Added Interactive Mode
1 parent 795a7fe commit 7ee6794

6 files changed

Lines changed: 162 additions & 30 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Designed for developers, researchers, and power users, DeepShell abstracts away
1515
* **Multi-LLM Support:**
1616
* Seamlessly connect to **Ollama** servers (local or remote).
1717
* Integrate with the **Google Gemini API**.
18+
* **Conversational Memory:**
19+
* Engage in multi-turn conversations using the new **interactive mode** (`-i`).
20+
* The model remembers the context of the last 10 turns of your conversation.
1821
* **Unified & Interactive Configuration:**
1922
* A central, user-friendly settings menu (`-s`) guides you through all configuration tasks.
2023
* Manages LLM service details, including server addresses (Ollama) and API keys (Gemini).
@@ -222,4 +225,3 @@ ds -v
222225
```
223226

224227
Happy Querying!
225-

dist/deepshell-v-1.0.4

8.68 MB
Binary file not shown.

gemini.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -320,37 +320,50 @@ def _setup_gemini_service(config_data):
320320
display_message(f"Gemini service configured with model: {chosen_model_full_name.split('/')[-1]}", "green")
321321
return config_data
322322

323-
def send_gemini_query(api_key, model_name, user_query, active_service_name_display, api_key_nickname_display, gemini_service_config):
323+
def send_gemini_query(api_key, model_name, user_query, conversation_history, active_service_name_display, api_key_nickname_display, gemini_service_config):
324324
"""
325325
Sends the user's query to the Gemini API and prints the response.
326326
Model name should be the full "models/gemini-pro" style name.
327327
If Markdown rendering is enabled in the service configuration, the response
328328
will be formatted using the `rich` library.
329+
`conversation_history` is a list of previous user/model messages.
330+
Returns the text of the response, or None on failure.
329331
gemini_service_config is the specific configuration part for Gemini service.
330332
"""
331333
model_display_name = model_name.split('/')[-1]
332334
sending_message = (
333335
f"Using active LLM service: {active_service_name_display} (API Key: '{api_key_nickname_display}'). "
334336
f"Sending query (Model: {model_display_name})..."
335337
)
336-
# display_message(sending_message, "blue", end='') # Old way
337-
display_message(sending_message, "blue") # New way, prints newline by default
338-
sys.stdout.flush() # Ensure the message is displayed before animation starts
338+
if not conversation_history:
339+
display_message(sending_message, "blue")
340+
sys.stdout.flush() # Ensure the message is displayed before animation starts
339341
message_len_for_animation = len(sending_message)
340342
if not model_name.startswith("models/"):
341343
model_name_for_api = f"models/{model_name}"
342344
else:
343345
model_name_for_api = model_name
344346

347+
# Transform canonical history to Gemini's format
348+
gemini_history = []
349+
for message in conversation_history:
350+
# Gemini uses 'model' for assistant role
351+
role = "model" if message["role"] == "model" else "user"
352+
gemini_history.append({
353+
"role": role,
354+
"parts": [{"text": message["content"]}]
355+
})
356+
# Add the new user query
357+
gemini_history.append({"role": "user", "parts": [{"text": user_query}]})
358+
345359
url = f"{GEMINI_API_BASE_URL}/{model_name_for_api}:generateContent?key={api_key}"
346-
payload = {
347-
"contents": [{"parts":[{"text": user_query}]}]
348-
}
360+
payload = {"contents": gemini_history}
349361

350362
stop_event = threading.Event()
351363
animation_thread = threading.Thread(target=_animate_progress, args=(stop_event, message_len_for_animation))
352364
animation_thread.daemon = True
353-
animation_thread.start()
365+
if not conversation_history: # Only animate on first query
366+
animation_thread.start()
354367

355368
response_data = None
356369
error_message_to_display = None
@@ -370,9 +383,10 @@ def send_gemini_query(api_key, model_name, user_query, active_service_name_displ
370383
error_message_to_display = "Error: Could not parse JSON response from Gemini API."
371384
finally:
372385
stop_event.set()
373-
animation_thread.join(timeout=0.5)
374-
sys.stdout.write("\r" + " " * message_len_for_animation + "\r")
375-
sys.stdout.flush()
386+
if not conversation_history: # Only join/clear if started
387+
animation_thread.join(timeout=0.5)
388+
sys.stdout.write("\r" + " " * message_len_for_animation + "\r")
389+
sys.stdout.flush()
376390

377391
if response_data and 'candidates' in response_data and response_data['candidates'] and \
378392
'content' in response_data['candidates'][0] and \
@@ -392,9 +406,14 @@ def send_gemini_query(api_key, model_name, user_query, active_service_name_displ
392406
else:
393407
print(gemini_text_response)
394408
display_message("-----------------------", "green")
409+
return gemini_text_response
395410
elif error_message_to_display:
396411
display_message(error_message_to_display, "red")
412+
return None
397413
elif not response_data and not error_message_to_display:
398414
display_message("Error: Received an empty or unexpected response from Gemini API.", "orange")
415+
return None
399416
elif response_data:
400417
display_message(f"Error: Unexpected response format from Gemini. Full response: {json.dumps(response_data, indent=2)}", "orange")
418+
return None
419+
return None

main.py

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from settings import jump_to_previous_llm # Import the new function
1919

2020
# Version
21-
__version__ = "1.0.3"
21+
__version__ = "1.0.4"
2222

2323
class CustomHelpFormatter(argparse.RawTextHelpFormatter):
2424
"""Custom formatter for argparse help messages to adjust spacing."""
@@ -130,6 +130,68 @@ def set_gemini_api_key_interactive(config_dir, config_file):
130130
setup_config(config_dir, config_file, jump_to="gemini_keys", is_direct_flag_call=True)
131131
sys.exit(0)
132132

133+
def start_interactive_session(config_data, config_dir, config_file):
134+
"""Starts an interactive chat session with the configured LLM."""
135+
display_message("\n--- DeepShell Interactive Mode ---", "green")
136+
display_message("Type 'exit' or 'quit' to end the session.", "yellow")
137+
138+
# Load service details from config
139+
active_service_name = config_data.get("active_llm_service")
140+
service_config = config_data["llm_services"][active_service_name]
141+
model_name = service_config.get("model")
142+
active_service_name_display = active_service_name.capitalize()
143+
144+
# Prepare service-specific details
145+
api_key_value, api_key_nickname = (None, None)
146+
server_address = None
147+
if active_service_name == LLM_SERVICE_GEMINI:
148+
api_key_value, api_key_nickname = _get_active_gemini_key_value(service_config)
149+
if not api_key_value:
150+
display_message("No active Gemini API key found. Please configure it via --setup.", "red")
151+
sys.exit(1)
152+
elif active_service_name == LLM_SERVICE_OLLAMA:
153+
server_address = service_config.get("server_address")
154+
if not server_address:
155+
display_message("Ollama server address not configured. Please configure it via --setup.", "red")
156+
sys.exit(1)
157+
158+
conversation_history = []
159+
MAX_HISTORY_TURNS = 10 # 10 pairs of user/model messages
160+
MAX_HISTORY_ITEMS = MAX_HISTORY_TURNS * 2
161+
162+
while True:
163+
try:
164+
user_input = input("> ")
165+
if user_input.lower() in ['exit', 'quit']:
166+
display_message("Exiting interactive mode.", "blue")
167+
break
168+
if not user_input.strip():
169+
continue
170+
171+
# Trim history if it's too long. Keep the last 9 pairs and make room for the new one.
172+
if len(conversation_history) >= MAX_HISTORY_ITEMS:
173+
conversation_history = conversation_history[2:]
174+
175+
response_text = None
176+
if active_service_name == LLM_SERVICE_GEMINI:
177+
response_text = send_gemini_query(
178+
api_key_value, model_name, user_input, conversation_history,
179+
active_service_name_display, api_key_nickname, service_config
180+
)
181+
elif active_service_name == LLM_SERVICE_OLLAMA:
182+
response_text = send_ollama_query(
183+
server_address, model_name, user_input, conversation_history,
184+
active_service_name_display, service_config
185+
)
186+
187+
if response_text:
188+
# Add to history using a canonical format
189+
conversation_history.append({"role": "user", "content": user_input})
190+
conversation_history.append({"role": "model", "content": response_text})
191+
192+
except EOFError: # Handle Ctrl+D
193+
display_message("\nExiting interactive mode.", "blue")
194+
break
133195

134196
def main():
135197
"""
@@ -151,13 +213,21 @@ def main():
151213
dest="model_change",
152214
help="Change the default model for the active LLM service."
153215
)
154-
parser.add_argument(
216+
217+
# Group for mutually exclusive query/interactive modes
218+
mode_group = parser.add_mutually_exclusive_group()
219+
mode_group.add_argument(
155220
"-q", "--query",
156221
nargs='+',
157222
metavar="QUERY",
158223
help="The query to send to the LLM. All text following this flag will be treated as the query.\n"
159224
"Example: deepshell -q What is the capital of France?"
160225
)
226+
mode_group.add_argument(
227+
"-i", "--interactive",
228+
action="store_true",
229+
help="Start an interactive chat session."
230+
)
161231
parser.add_argument(
162232
"-d", "--delete-config",
163233
action="store_true",
@@ -249,6 +319,18 @@ def main():
249319
jump_to_previous_llm(config_dir, config_file)
250320
sys.exit(0)
251321

322+
if args.interactive:
323+
config_data = load_config(config_file)
324+
if config_data is None or not config_data.get("active_llm_service"):
325+
display_message("Configuration not found or no active LLM service. Running setup...", "yellow")
326+
config_data = setup_config(config_dir, config_file, is_direct_flag_call=False)
327+
if not config_data or not config_data.get("active_llm_service"):
328+
display_message("Setup incomplete. Exiting.", "red")
329+
sys.exit(1)
330+
331+
start_interactive_session(config_data, config_dir, config_file)
332+
sys.exit(0)
333+
252334
config_data = load_config(config_file)
253335
user_query_list = args.query
254336
user_query = " ".join(user_query_list) if user_query_list else None
@@ -290,14 +372,14 @@ def main():
290372
if not server_address:
291373
display_message("Ollama server address not configured. Please run --setup or --llm to configure Ollama.", "red")
292374
sys.exit(1)
293-
send_ollama_query(server_address, model_name, user_query, active_service_name_display, service_config)
375+
send_ollama_query(server_address, model_name, user_query, [], active_service_name_display, service_config)
294376
elif active_service_name == LLM_SERVICE_GEMINI:
295377
active_api_key_value, active_nickname = _get_active_gemini_key_value(service_config)
296378
if not active_api_key_value:
297379
display_message("No active Gemini API key configured or found. Please run --setup or --llm to configure Gemini.", "red")
298380
sys.exit(1)
299381
send_gemini_query(
300-
active_api_key_value, model_name, user_query,
382+
active_api_key_value, model_name, user_query, [],
301383
active_service_name_display,
302384
active_nickname,
303385
service_config

modules.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
requests
22
chardet
3-
nuitka
43
rich
4+
argparse
5+
textwrap
6+
os
7+
sys
8+
subprocess
9+
shutil
10+
json
11+
requests
12+
threading
13+
time
14+
pyinstaller
15+

ollama.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,32 +132,45 @@ def _setup_ollama_service(config_data, server_address_input_prompt="Enter OLLAMA
132132
display_message(f"Ollama service configured with model: {chosen_model}", "green")
133133
return config_data
134134

135-
def send_ollama_query(server_address, model_name, user_query, active_service_name_display, ollama_service_config):
135+
def send_ollama_query(server_address, model_name, user_query, conversation_history, active_service_name_display, ollama_service_config):
136136
"""
137137
Sends the user's query to the OLLAMA server's /api/chat endpoint
138138
and prints only the relevant response content.
139+
`conversation_history` is a list of previous user/model messages.
140+
Returns the text of the response, or None on failure.
139141
ollama_service_config is the specific configuration part for Ollama service.
140142
"""
141143
sending_message = (
142144
f"Using active LLM service: {active_service_name_display}. "
143145
f"Sending query to {server_address} (Model: {model_name})..."
144146
)
145-
# display_message(sending_message, "blue", end='') # Old way
146-
display_message(sending_message, "blue") # New way, prints newline by default
147-
sys.stdout.flush() # Ensure the message is displayed before animation starts
147+
if not conversation_history:
148+
display_message(sending_message, "blue")
149+
sys.stdout.flush() # Ensure the message is displayed before animation starts
148150
message_len_for_animation = len(sending_message)
149151

152+
# Transform canonical history to Ollama's format
153+
ollama_messages = []
154+
for message in conversation_history:
155+
# Ollama uses 'assistant' for model role
156+
role = "assistant" if message["role"] == "model" else "user"
157+
ollama_messages.append({
158+
"role": role,
159+
"content": message["content"]
160+
})
161+
# Add the new user query
162+
ollama_messages.append({"role": "user", "content": user_query})
163+
150164
url = f"{server_address}/api/chat"
151165
payload = {
152-
"model": model_name,
153-
"messages": [{"role": "user", "content": user_query}],
154-
"stream": False
166+
"model": model_name, "messages": ollama_messages, "stream": False
155167
}
156168

157169
stop_event = threading.Event()
158170
animation_thread = threading.Thread(target=_animate_progress, args=(stop_event, message_len_for_animation))
159-
animation_thread.daemon = True
160-
animation_thread.start()
171+
animation_thread.daemon = True
172+
if not conversation_history: # Only animate on first query
173+
animation_thread.start()
161174

162175
response_data = None
163176
error_message_to_display = None
@@ -181,12 +194,14 @@ def send_ollama_query(server_address, model_name, user_query, active_service_nam
181194
error_message_to_display = f"An unexpected error occurred during query: {e}"
182195
finally:
183196
stop_event.set()
184-
animation_thread.join(timeout=0.5)
185-
sys.stdout.write("\r" + " " * message_len_for_animation + "\r")
186-
sys.stdout.flush()
197+
if not conversation_history: # Only join/clear if started
198+
animation_thread.join(timeout=0.5)
199+
sys.stdout.write("\r" + " " * message_len_for_animation + "\r")
200+
sys.stdout.flush()
187201

188202
if error_message_to_display:
189203
display_message(error_message_to_display, "red")
204+
return None
190205
elif response_data:
191206
if "message" in response_data and "content" in response_data["message"]:
192207
ollama_text_response = response_data["message"]["content"].strip()
@@ -203,5 +218,8 @@ def send_ollama_query(server_address, model_name, user_query, active_service_nam
203218
else:
204219
print(ollama_text_response)
205220
display_message("-----------------------", "green")
221+
return ollama_text_response
206222
else:
207-
display_message(f"Error: Unexpected response format from server. Full response: {json.dumps(response_data, indent=2)}", "orange")
223+
display_message(f"Error: Unexpected response format from server. Full response: {json.dumps(response_data, indent=2)}", "orange")
224+
return None
225+
return None

0 commit comments

Comments
 (0)