askme-ai-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
askme.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
import requests
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
import shlex
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.prompt import Confirm, Prompt
|
|
16
|
+
from rich.live import Live
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
def apply_patch(filepath, diff_content):
|
|
22
|
+
"""Applies a unified diff to a file using the system patch utility."""
|
|
23
|
+
try:
|
|
24
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.diff', delete=False) as tmp:
|
|
25
|
+
tmp.write(diff_content)
|
|
26
|
+
tmp_path = tmp.name
|
|
27
|
+
|
|
28
|
+
# -u treats it as unified diff, -N allows new files
|
|
29
|
+
result = subprocess.run(['patch', '-u', filepath, tmp_path], capture_output=True, text=True)
|
|
30
|
+
os.unlink(tmp_path)
|
|
31
|
+
|
|
32
|
+
if result.returncode == 0:
|
|
33
|
+
return f"Successfully applied patch to {filepath}."
|
|
34
|
+
else:
|
|
35
|
+
return f"Patch failed:\n{result.stderr}"
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return f"Error applying patch: {e}"
|
|
38
|
+
|
|
39
|
+
def summarize_history(history, api_key, model_name):
|
|
40
|
+
"""Calls the LLM to summarize interaction history into a concise project state."""
|
|
41
|
+
url = "https://ollama.com/api/generate"
|
|
42
|
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
43
|
+
history_text = "\n".join([f"User: {h['q']}\nAgent: {h['a']}" for h in history])
|
|
44
|
+
summary_prompt = f"Summarize this interaction history into a single concise paragraph describing the current 'State of the Project'. Mention key accomplishments and current file states.\n\n[HISTORY]\n{history_text}"
|
|
45
|
+
|
|
46
|
+
payload = {"model": model_name, "prompt": summary_prompt, "stream": False}
|
|
47
|
+
try:
|
|
48
|
+
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
|
49
|
+
return response.json().get("response", "Summary unavailable.")
|
|
50
|
+
except Exception:
|
|
51
|
+
return "Summary generation failed."
|
|
52
|
+
|
|
53
|
+
def log_execution(command, returncode, duration, session="default"):
|
|
54
|
+
"""Records execution details to a session log file."""
|
|
55
|
+
log_dir = ".askme/logs"
|
|
56
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
57
|
+
log_path = os.path.join(log_dir, f"{session}.log")
|
|
58
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
59
|
+
with open(log_path, "a") as f:
|
|
60
|
+
f.write(f"[{timestamp}] CMD: {command} | EXIT: {returncode} | DUR: {duration:.2f}s\n")
|
|
61
|
+
|
|
62
|
+
def load_history(history_file):
|
|
63
|
+
"""Loads conversation history from the specified file."""
|
|
64
|
+
try:
|
|
65
|
+
with open(history_file, "r") as f:
|
|
66
|
+
return json.load(f)
|
|
67
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
68
|
+
return []
|
|
69
|
+
except Exception as e:
|
|
70
|
+
console.print(f"[yellow]Warning: Could not load history: {e}[/yellow]")
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
def save_history(history, history_file):
|
|
74
|
+
"""Saves the last 10 interactions to maintain project context."""
|
|
75
|
+
with open(history_file, "w") as f:
|
|
76
|
+
json.dump(history[-10:], f)
|
|
77
|
+
|
|
78
|
+
def get_directory_tree(path=".", prefix="", depth=0, max_depth=2):
|
|
79
|
+
"""Generates a visual directory tree string."""
|
|
80
|
+
if depth > max_depth:
|
|
81
|
+
return []
|
|
82
|
+
tree = []
|
|
83
|
+
try:
|
|
84
|
+
items = sorted(os.listdir(path))
|
|
85
|
+
# Filter out common junk and hidden files except history
|
|
86
|
+
items = [i for i in items if not i.startswith('.') or i == '.askme']
|
|
87
|
+
for i, item in enumerate(items):
|
|
88
|
+
connector = "└── " if i == len(items) - 1 else "├── "
|
|
89
|
+
tree.append(f"{prefix}{connector}{item}")
|
|
90
|
+
full_path = os.path.join(path, item)
|
|
91
|
+
if os.path.isdir(full_path):
|
|
92
|
+
extension = " " if i == len(items) - 1 else "│ "
|
|
93
|
+
tree.extend(get_directory_tree(full_path, prefix + extension, depth + 1))
|
|
94
|
+
except Exception: pass
|
|
95
|
+
return tree
|
|
96
|
+
|
|
97
|
+
def get_system_context():
|
|
98
|
+
"""Gathers environment info to improve agent awareness."""
|
|
99
|
+
return {
|
|
100
|
+
"os": platform.system(),
|
|
101
|
+
"os_version": platform.release(),
|
|
102
|
+
"python_version": sys.version.split()[0],
|
|
103
|
+
"cwd": os.getcwd()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def is_dangerous(command):
|
|
107
|
+
"""Checks if a command contains potentially destructive operations using regex."""
|
|
108
|
+
dangerous_patterns = [
|
|
109
|
+
r"rm\s+-rf\s+/", r"rm\s+-rf\s+\*", r"rm\s+-rf\s+\.",
|
|
110
|
+
r"> /dev/sd", r"mkfs", r"chmod\s+-R\s+777",
|
|
111
|
+
r"dd\s+if=", r":\(\)\{\s+:\|:&\s+\};:",
|
|
112
|
+
r"mv\s+.*\s+/dev/null", r"shutdown", r"reboot"
|
|
113
|
+
]
|
|
114
|
+
return any(re.search(pattern, command) for pattern in dangerous_patterns)
|
|
115
|
+
|
|
116
|
+
def is_filesystem_modifying(command):
|
|
117
|
+
"""Checks if a command is likely to modify the directory structure."""
|
|
118
|
+
modifiers = ["mkdir", "touch", "rm", "mv", "cp", "git", ">>", ">"]
|
|
119
|
+
return any(mod in command for mod in modifiers)
|
|
120
|
+
|
|
121
|
+
def is_path_safe(command):
|
|
122
|
+
"""Prevents directory traversal and access to absolute system paths."""
|
|
123
|
+
# Basic jail: block climbing up or absolute paths that don't start with CWD
|
|
124
|
+
return ".." not in command and not (command.startswith("/") and not command.startswith(os.getcwd()))
|
|
125
|
+
|
|
126
|
+
def get_current_context():
|
|
127
|
+
"""Refreshes the directory tree and system info for the prompt."""
|
|
128
|
+
try:
|
|
129
|
+
tree = get_directory_tree()
|
|
130
|
+
sys_ctx = get_system_context()
|
|
131
|
+
return (
|
|
132
|
+
f"Environment: {sys_ctx['os']} {sys_ctx['os_version']}, Python {sys_ctx['python_version']}\n"
|
|
133
|
+
f"CWD: {sys_ctx['cwd']}\nProject Structure:\n" + "\n".join(tree)
|
|
134
|
+
)
|
|
135
|
+
except Exception:
|
|
136
|
+
return "Unknown directory context."
|
|
137
|
+
|
|
138
|
+
def main():
|
|
139
|
+
# 1. Setup argument parsing
|
|
140
|
+
parser = argparse.ArgumentParser(description="askme - The Professional CLI AI Agent")
|
|
141
|
+
parser.add_argument("-p", "--prompt", type=str, nargs='+', help="The prompt to send to the model")
|
|
142
|
+
parser.add_argument("-s", "--session", type=str, default="default", help="Session name for the history")
|
|
143
|
+
parser.add_argument("-m", "--model", type=str, default="gemma4:31b-cloud", help="Ollama model to use")
|
|
144
|
+
parser.add_argument("-d", "--dry-run", action="store_true", help="Show proposed commands without executing them")
|
|
145
|
+
args = parser.parse_args()
|
|
146
|
+
|
|
147
|
+
# 2. Configuration for Ollama
|
|
148
|
+
api_key = os.getenv("OLLAMA_API_KEY")
|
|
149
|
+
url = "https://ollama.com/api/generate"
|
|
150
|
+
|
|
151
|
+
console.print("[bold blue]Verifying Environment...[/bold blue]")
|
|
152
|
+
|
|
153
|
+
if not api_key:
|
|
154
|
+
console.print("[bold yellow]OLLAMA_API_KEY not found in environment.[/bold yellow]")
|
|
155
|
+
api_key = Prompt.ask("[bold cyan]Please enter your Ollama API Key[/bold cyan]", password=True)
|
|
156
|
+
if not api_key:
|
|
157
|
+
console.print("[bold red]Error: API Key is required to proceed.[/bold red]")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Prompt for model selection if key was missing
|
|
161
|
+
console.print("\n[bold white]Select an AI Model to use:[/bold white]")
|
|
162
|
+
console.print("1. gemma4:31b-cloud (Default)")
|
|
163
|
+
console.print("2. llama3")
|
|
164
|
+
console.print("3. mistral")
|
|
165
|
+
choice = Prompt.ask("Choose a number or type a custom model name", default="1")
|
|
166
|
+
|
|
167
|
+
if choice == "1": args.model = "gemma4:31b-cloud"
|
|
168
|
+
elif choice == "2": args.model = "llama3"
|
|
169
|
+
elif choice == "3": args.model = "mistral"
|
|
170
|
+
else: args.model = choice
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
requests.head(url.rsplit('/', 1)[0], timeout=5)
|
|
174
|
+
console.print(f"[green]✔[/green] OLLAMA_API_KEY detected and endpoint reachable.")
|
|
175
|
+
console.print(f"[green]✔[/green] Using model: [bold cyan]{args.model}[/bold cyan]\n")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
console.print(f"[bold yellow]![/bold yellow] Warning: Connectivity check to {url} failed: {e}\n")
|
|
178
|
+
|
|
179
|
+
# 2.1 Define Agent behavior for iterative reasoning
|
|
180
|
+
system_instruction = (
|
|
181
|
+
"You are an iterative CLI Agent. Solve user requests by planning and executing shell commands.\n"
|
|
182
|
+
"1. Analyze context and provide reasoning.\n"
|
|
183
|
+
"2. For standard execution, use <execute>command</execute> (shlex-safe).\n"
|
|
184
|
+
"3. For complex commands with pipes or redirects, use <execute_shell>command</execute_shell>.\n"
|
|
185
|
+
"4. For large files, prefer using 'grep', 'head', or 'tail' to read specific sections.\n"
|
|
186
|
+
"5. Use command output to decide your next step.\n"
|
|
187
|
+
"6. Use <patch file=\"path\">unified diff content</patch> to modify files precisely without overwriting.\n"
|
|
188
|
+
"7. When finished, provide a final response."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# 2.2 Setup Session and Load History
|
|
192
|
+
session_dir = ".askme/sessions"
|
|
193
|
+
os.makedirs(session_dir, exist_ok=True)
|
|
194
|
+
history_path = os.path.join(session_dir, f"{args.session}.json")
|
|
195
|
+
history = load_history(history_path)
|
|
196
|
+
|
|
197
|
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
198
|
+
session_vars = {} # Maintain environment variables across interactive turns
|
|
199
|
+
|
|
200
|
+
# 3. Determine if we are in one-shot or interactive mode
|
|
201
|
+
if args.prompt:
|
|
202
|
+
user_prompt = " ".join(args.prompt) if isinstance(args.prompt, list) else args.prompt
|
|
203
|
+
run_agent_loop(user_prompt, system_instruction, url, headers, args, history, history_path, api_key, session_vars, args.model)
|
|
204
|
+
else:
|
|
205
|
+
console.print(Panel("[bold cyan]Entering Interactive Mode[/bold cyan]\nType [bold red]'exit'[/bold red] or [bold red]'quit'[/bold red] to stop.", border_style="blue"))
|
|
206
|
+
while True:
|
|
207
|
+
user_prompt = Prompt.ask("[bold yellow]Query[/bold yellow]")
|
|
208
|
+
if user_prompt.lower() in ["exit", "quit"]:
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
run_agent_loop(user_prompt, system_instruction, url, headers, args, history, history_path, api_key, session_vars, args.model)
|
|
212
|
+
|
|
213
|
+
def run_agent_loop(user_prompt, system_instruction, url, headers, args, history, history_path, api_key, session_vars, model_name):
|
|
214
|
+
# Refresh context and summary at the start of every loop turn
|
|
215
|
+
project_context = get_current_context()
|
|
216
|
+
summary_block = ""
|
|
217
|
+
if len(history) >= 10:
|
|
218
|
+
console.print("[dim]Session history is long. Generating semantic summary to save tokens...[/dim]")
|
|
219
|
+
summary = summarize_history(history[:-3], api_key, model_name)
|
|
220
|
+
summary_block = f"[PROJECT SUMMARY]\n{summary}\n\n"
|
|
221
|
+
# Update history reference to keep the most recent 3 for immediate context
|
|
222
|
+
history[:] = history[-3:]
|
|
223
|
+
|
|
224
|
+
history_str = "\n".join([f"User: {h['q']}\nAgent: {h['a']}" for h in history])
|
|
225
|
+
current_prompt = f"{system_instruction}\n\n[PROJECT CONTEXT]\n{project_context}\n\n{summary_block}[RECENT HISTORY]\n{history_str}\n\nUser: {user_prompt}"
|
|
226
|
+
all_agent_responses = []
|
|
227
|
+
iteration = 0
|
|
228
|
+
max_iterations = 5
|
|
229
|
+
tree_dirty = False
|
|
230
|
+
|
|
231
|
+
total_prompt_tokens = 0
|
|
232
|
+
total_completion_tokens = 0
|
|
233
|
+
start_session_time = datetime.now()
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
while iteration < max_iterations:
|
|
237
|
+
iteration += 1
|
|
238
|
+
payload = {
|
|
239
|
+
"model": model_name,
|
|
240
|
+
"prompt": current_prompt,
|
|
241
|
+
"stream": True,
|
|
242
|
+
"options": {
|
|
243
|
+
"num_ctx": 8192,
|
|
244
|
+
"temperature": 0.2 # Lower temperature for more precise CLI commands
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
response = requests.post(url, headers=headers, json=payload, stream=True)
|
|
248
|
+
response.raise_for_status()
|
|
249
|
+
|
|
250
|
+
console.print(f"\n[bold cyan] askme Agent[/bold cyan]: ", end="")
|
|
251
|
+
full_content = ""
|
|
252
|
+
for line in response.iter_lines():
|
|
253
|
+
if line:
|
|
254
|
+
chunk = json.loads(line.decode("utf-8"))
|
|
255
|
+
if "response" in chunk:
|
|
256
|
+
token = chunk["response"]
|
|
257
|
+
console.print(token, end="", style="italic green")
|
|
258
|
+
full_content += token
|
|
259
|
+
if chunk.get("done"):
|
|
260
|
+
total_prompt_tokens += chunk.get("prompt_eval_count", 0)
|
|
261
|
+
total_completion_tokens += chunk.get("eval_count", 0)
|
|
262
|
+
print()
|
|
263
|
+
all_agent_responses.append(full_content)
|
|
264
|
+
|
|
265
|
+
# Parse for the command to execute
|
|
266
|
+
# Uses regex to find commands even inside code blocks or with trailing spaces
|
|
267
|
+
command = None
|
|
268
|
+
use_shell = False
|
|
269
|
+
|
|
270
|
+
shell_match = re.search(r"<execute_shell>(.*?)</execute_shell>", full_content, re.DOTALL)
|
|
271
|
+
exec_match = re.search(r"<execute>(.*?)</execute>", full_content, re.DOTALL)
|
|
272
|
+
patch_match = re.search(r"<patch file=['\"](.*?)['\"]>(.*?)</patch>", full_content, re.DOTALL)
|
|
273
|
+
|
|
274
|
+
if shell_match:
|
|
275
|
+
command = shell_match.group(1).strip()
|
|
276
|
+
use_shell = True
|
|
277
|
+
elif patch_match:
|
|
278
|
+
filepath, diff_content = patch_match.groups()
|
|
279
|
+
res = apply_patch(filepath, diff_content)
|
|
280
|
+
current_prompt += f"\nAgent: {full_content}\n[SYSTEM: Patch Result]\n{res}\nNext step:"
|
|
281
|
+
tree_dirty = True
|
|
282
|
+
continue
|
|
283
|
+
elif exec_match:
|
|
284
|
+
command = exec_match.group(1).strip()
|
|
285
|
+
use_shell = False
|
|
286
|
+
|
|
287
|
+
if not command:
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
# The Shield: Safety check for dangerous commands
|
|
291
|
+
if is_dangerous(command) or not is_path_safe(command):
|
|
292
|
+
console.print(Panel(
|
|
293
|
+
f"[bold red]SECURITY ALERT:[/bold red] Unsafe command or path detected!\n"
|
|
294
|
+
f"Command: [bold cyan]{command}[/bold cyan]\n\n"
|
|
295
|
+
"Execution blocked for safety.",
|
|
296
|
+
title="[blink red]SECURITY BLOCK[/blink red]",
|
|
297
|
+
border_style="bold red"
|
|
298
|
+
))
|
|
299
|
+
if not Confirm.ask("[bold red]Are you absolutely sure you want to run this?[/bold red]", default=False):
|
|
300
|
+
console.print("[bold yellow]! High-risk execution blocked by user.[/bold yellow]")
|
|
301
|
+
break
|
|
302
|
+
|
|
303
|
+
if args.dry_run:
|
|
304
|
+
console.print(Panel(f"DRY RUN: Would execute [bold cyan]{command}[/bold cyan]", border_style="yellow"))
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
mode_str = " (SHELL MODE)" if use_shell else ""
|
|
308
|
+
if Confirm.ask(f"\n[bold yellow] Agent suggests{mode_str}:[/bold yellow] [bold blue]{command}[/bold blue]\n[dim]Execute?[/dim]"):
|
|
309
|
+
console.print(Panel(f"Executing: [bold white]{command}[/bold white]", border_style="blue", title="System" if not use_shell else "System (Shell)"))
|
|
310
|
+
|
|
311
|
+
output_text = ""
|
|
312
|
+
try:
|
|
313
|
+
if use_shell:
|
|
314
|
+
cmd_args = command
|
|
315
|
+
else:
|
|
316
|
+
cmd_args = shlex.split(command)
|
|
317
|
+
|
|
318
|
+
start_time = datetime.now()
|
|
319
|
+
# Merge system env with session variables
|
|
320
|
+
env = os.environ.copy()
|
|
321
|
+
env.update(session_vars)
|
|
322
|
+
|
|
323
|
+
process = subprocess.Popen(
|
|
324
|
+
cmd_args, shell=use_shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
325
|
+
text=True, bufsize=1, env=env
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
console.print("[dim]Command Output:[/dim]")
|
|
329
|
+
with Live(Text(""), refresh_per_second=4) as live:
|
|
330
|
+
for line in process.stdout:
|
|
331
|
+
output_text += line
|
|
332
|
+
display_text = "\n".join(output_text.splitlines()[-10:])
|
|
333
|
+
live.update(Text(display_text)) # Show last 10 lines
|
|
334
|
+
|
|
335
|
+
process.wait(timeout=30) # Prevent hanging on interactive commands
|
|
336
|
+
duration = (datetime.now() - start_time).total_seconds()
|
|
337
|
+
|
|
338
|
+
log_execution(command, process.returncode, duration, args.session)
|
|
339
|
+
|
|
340
|
+
if process.returncode == 0:
|
|
341
|
+
console.print("[bold green]✔ Success.[/bold green]")
|
|
342
|
+
system_status = "Command result"
|
|
343
|
+
else:
|
|
344
|
+
console.print(f"[bold red]✘ Failed with code {process.returncode}[/bold red]")
|
|
345
|
+
system_status = f"Command failed with exit code {process.returncode}. Analyze the error below."
|
|
346
|
+
|
|
347
|
+
tree_dirty = is_filesystem_modifying(command)
|
|
348
|
+
|
|
349
|
+
except subprocess.TimeoutExpired:
|
|
350
|
+
process.kill()
|
|
351
|
+
console.print("[bold red]✘ Command timed out (30s).[/bold red]")
|
|
352
|
+
output_text += "\n[ERROR: Process terminated due to timeout]"
|
|
353
|
+
system_status = "Command timed out. Use a more targeted command."
|
|
354
|
+
except Exception as e:
|
|
355
|
+
console.print(f"[bold red]✘ Execution error:[/bold red] {e}")
|
|
356
|
+
output_text += f"\n[ERROR: {e}]"
|
|
357
|
+
system_status = "Execution error"
|
|
358
|
+
|
|
359
|
+
# Token Optimization: Truncate very long outputs in the prompt
|
|
360
|
+
if len(output_text) > 4000:
|
|
361
|
+
output_text = output_text[:2000] + "\n... [Output truncated for brevity] ...\n" + output_text[-2000:]
|
|
362
|
+
|
|
363
|
+
# Update context with output for the next reasoning step
|
|
364
|
+
current_prompt += f"\nAgent: {full_content}\n[SYSTEM: {system_status}]\n{output_text}"
|
|
365
|
+
|
|
366
|
+
if tree_dirty:
|
|
367
|
+
new_tree = get_directory_tree()
|
|
368
|
+
current_prompt += f"\n[UPDATED PROJECT STRUCTURE]\n" + "\n".join(new_tree)
|
|
369
|
+
|
|
370
|
+
current_prompt += "\nNext step:"
|
|
371
|
+
else:
|
|
372
|
+
console.print("[bold yellow]! Execution skipped by user.[/bold yellow]")
|
|
373
|
+
break
|
|
374
|
+
|
|
375
|
+
if iteration >= max_iterations:
|
|
376
|
+
console.print(Panel("[yellow]Maximum iteration limit reached for this request.[/yellow]", border_style="yellow"))
|
|
377
|
+
|
|
378
|
+
# Display Metadata Summary
|
|
379
|
+
end_session_time = datetime.now()
|
|
380
|
+
total_duration = (end_session_time - start_session_time).total_seconds()
|
|
381
|
+
|
|
382
|
+
summary_text = Text()
|
|
383
|
+
summary_text.append(f"Total Iterations: ", style="bold white")
|
|
384
|
+
summary_text.append(f"{iteration}\n", style="cyan")
|
|
385
|
+
summary_text.append(f"Prompt Tokens: ", style="bold white")
|
|
386
|
+
summary_text.append(f"{total_prompt_tokens} ", style="cyan")
|
|
387
|
+
summary_text.append(f"| Completion Tokens: ", style="bold white")
|
|
388
|
+
summary_text.append(f"{total_completion_tokens}\n", style="cyan")
|
|
389
|
+
summary_text.append(f"Total Time: ", style="bold white")
|
|
390
|
+
summary_text.append(f"{total_duration:.2f}s", style="cyan")
|
|
391
|
+
|
|
392
|
+
console.print(Panel(summary_text, title="[bold magenta]Session Metadata[/bold magenta]", border_style="magenta"))
|
|
393
|
+
|
|
394
|
+
# Save the cumulative interaction
|
|
395
|
+
history.append({"q": user_prompt, "a": "\n".join(all_agent_responses)})
|
|
396
|
+
save_history(history, history_path)
|
|
397
|
+
|
|
398
|
+
except KeyboardInterrupt:
|
|
399
|
+
console.print("\n[bold red]Operation cancelled by user.[/bold red]")
|
|
400
|
+
except Exception as e:
|
|
401
|
+
console.print(f"\n[bold red]Error occurred:[/bold red] {e}")
|
|
402
|
+
|
|
403
|
+
if __name__ == "__main__":
|
|
404
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: askme-ai-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A professional iterative CLI AI Agent powered by Ollama
|
|
5
|
+
Author-email: Tharun Kumar <buddetharunkumar123@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: ollama>=0.6.2
|
|
16
|
+
Requires-Dist: rich>=13.7.0
|
|
17
|
+
Requires-Dist: requests>=2.31.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
askme.py,sha256=ZetmHlSFNm3Wi4WsnN7QKUxbKtpiDPv9hLxYRT7kqxQ,19152
|
|
2
|
+
askme_ai_cli-0.1.0.dist-info/METADATA,sha256=GxU7MDLFHy7nsi1hZ2kh8Ij6595diZc8E4Db1EX1SGQ,631
|
|
3
|
+
askme_ai_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
askme_ai_cli-0.1.0.dist-info/entry_points.txt,sha256=D9UBijt6_ealGqOY6lRh1ws59XOYXDkt9mjfOHGEa3o,37
|
|
5
|
+
askme_ai_cli-0.1.0.dist-info/top_level.txt,sha256=-pXk5qNHQqrOOtugfdlfoUINWhsTxhhoHzl1J9MCXVk,6
|
|
6
|
+
askme_ai_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
askme
|