tass 0.1.0__py3-none-any.whl → 0.1.2__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.
src/app.py CHANGED
@@ -29,6 +29,56 @@ class TassApp:
29
29
  "read_file": self.read_file,
30
30
  }
31
31
 
32
+ def _check_llm_host(self):
33
+ test_url = f"{self.host}/v1/models"
34
+ try:
35
+ response = requests.get(test_url, timeout=2)
36
+ console.print("Terminal Assistant [green](LLM connection ✓)[/green]")
37
+ if response.status_code == 200:
38
+ return
39
+ except Exception:
40
+ console.print("Terminal Assistant [red](LLM connection ✗)[/red]")
41
+
42
+ console.print("\n[red]Could not connect to LLM[/red]")
43
+ console.print(f"If your LLM isn't running on {self.host}, you can set the [bold]TASS_HOST[/] environment variable to a different URL.")
44
+ new_host = console.input(
45
+ "Enter a different URL for this session (or press Enter to keep current): "
46
+ ).strip()
47
+
48
+ if new_host:
49
+ self.host = new_host
50
+
51
+ try:
52
+ response = requests.get(f"{self.host}/v1/models", timeout=2)
53
+ if response.status_code == 200:
54
+ console.print(f"[green]Connection established to {self.host}[/green]")
55
+ return
56
+ except Exception:
57
+ console.print(f"[red]Unable to verify new host {self.host}. Continuing with it anyway.[/red]")
58
+
59
+ def summarize(self):
60
+ max_messages = 10
61
+ if len(self.messages) <= max_messages:
62
+ return
63
+
64
+ prompt = (
65
+ "The conversation is becoming long and might soon go beyond the "
66
+ "context limit. Please provide a concise summary of the conversation, "
67
+ "preserving all important details. Keep the summary short enough "
68
+ "to fit within a few paragraphs at the most."
69
+ )
70
+
71
+ response = requests.post(
72
+ f"{self.host}/v1/chat/completions",
73
+ json={
74
+ "messages": self.messages + [{"role": "user", "content": prompt}],
75
+ "chat_template_kwargs": {"reasoning_effort": "medium"},
76
+ },
77
+ )
78
+ data = response.json()
79
+ summary = data["choices"][0]["message"]["content"]
80
+ self.messages = [self.messages[0], {"role": "assistant", "content": f"Summary of the conversation so far:\n{summary}"}]
81
+
32
82
  def call_llm(self) -> str | None:
33
83
  response = requests.post(
34
84
  f"{self.host}/v1/chat/completions",
@@ -42,38 +92,40 @@ class TassApp:
42
92
  )
43
93
 
44
94
  data = response.json()
45
- if "tool_calls" in data["choices"][0]["message"]:
46
- tool_name = data["choices"][0]["message"]["tool_calls"][0]["function"]["name"]
47
- tool_args_str = data["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"]
48
- self.messages.append(
49
- {
50
- "role": "assistant",
51
- "tool_calls": [
52
- {
53
- "index": 0,
54
- "id": "id1",
55
- "type": "function",
56
- "function": {
57
- "name": tool_name,
58
- "arguments": tool_args_str
59
- }
95
+ message = data["choices"][0]["message"]
96
+ if not message.get("tool_calls"):
97
+ return message["content"]
98
+
99
+ tool_name = message["tool_calls"][0]["function"]["name"]
100
+ tool_args_str = message["tool_calls"][0]["function"]["arguments"]
101
+ self.messages.append(
102
+ {
103
+ "role": "assistant",
104
+ "tool_calls": [
105
+ {
106
+ "index": 0,
107
+ "id": "id1",
108
+ "type": "function",
109
+ "function": {
110
+ "name": tool_name,
111
+ "arguments": tool_args_str
60
112
  }
61
- ]
62
- }
63
- )
64
- try:
65
- tool = self.TOOLS_MAP[tool_name]
66
- tool_args = json.loads(tool_args_str)
67
- result = tool(**tool_args)
68
- self.messages.append({"role": "tool", "content": result})
69
- except Exception as e:
70
- self.messages.append({"role": "user", "content": str(e)})
71
- return self.call_llm()
72
-
73
- return data["choices"][0]["message"]["content"]
113
+ }
114
+ ]
115
+ }
116
+ )
117
+ try:
118
+ tool = self.TOOLS_MAP[tool_name]
119
+ tool_args = json.loads(tool_args_str)
120
+ result = tool(**tool_args)
121
+ self.messages.append({"role": "tool", "content": result})
122
+ return None
123
+ except Exception as e:
124
+ self.messages.append({"role": "user", "content": str(e)})
125
+ return self.call_llm()
74
126
 
75
127
  def read_file(self, path: str, start: int = 1) -> str:
76
- console.print(f" └ Reading file [bold]{path}[/] (start={start})...")
128
+ console.print(f" └ Reading file [bold]{path}[/]...")
77
129
 
78
130
  lines = []
79
131
  with open(path) as f:
@@ -87,6 +139,7 @@ class TassApp:
87
139
  line_num += 1
88
140
 
89
141
  if len(lines) >= 1000:
142
+ lines.append("... (truncated)")
90
143
  break
91
144
 
92
145
  console.print(" [green]Command succeeded[/green]")
@@ -160,7 +213,11 @@ class TassApp:
160
213
  return f"Command output (exit {result.returncode}):\n{out}\n{err}"
161
214
 
162
215
  def run(self):
163
- console.print("Terminal Assistant")
216
+ try:
217
+ self._check_llm_host()
218
+ except KeyboardInterrupt:
219
+ console.print("\nBye!")
220
+ return
164
221
 
165
222
  while True:
166
223
  try:
@@ -179,8 +236,15 @@ class TassApp:
179
236
  self.messages.append({"role": "user", "content": user_input})
180
237
 
181
238
  while True:
182
- llm_resp = self.call_llm()
239
+ try:
240
+ llm_resp = self.call_llm()
241
+ except Exception as e:
242
+ console.print(f"Failed to call LLM: {str(e)}")
243
+ break
244
+
183
245
  if llm_resp is not None:
184
246
  console.print("")
185
247
  console.print(Markdown(llm_resp))
248
+ self.messages.append({"role": "assistant", "content": llm_resp})
249
+ self.summarize()
186
250
  break
src/constants.py CHANGED
@@ -3,12 +3,16 @@ from pathlib import Path
3
3
  _apply_patch_path = Path(__file__).resolve().parent / "third_party" / "apply_patch.md"
4
4
  APPLY_PATCH_PROMPT = _apply_patch_path.read_text().strip()
5
5
 
6
+ _cwd_path = Path.cwd().resolve()
7
+
6
8
  SYSTEM_PROMPT = f"""You are Terminal-Assistant, a helpful AI that executes shell commands based on natural-language requests.
7
9
 
8
10
  If the user's request involves making changes to the filesystem such as creating or deleting files or directories, you MUST first check whether the file or directory exists before proceeding.
9
11
 
10
12
  If a user asks for an answer or explanation to something instead of requesting to run a command, answer briefly and concisely. Do not supply extra information, suggestions, tips, or anything of the sort.
11
13
 
14
+ Current working directory: {_cwd_path}
15
+
12
16
  {APPLY_PATCH_PROMPT}"""
13
17
 
14
18
  TOOLS = [
@@ -26,7 +30,7 @@ TOOLS = [
26
30
  },
27
31
  "explanation": {
28
32
  "type": "string",
29
- "description": "A brief explanation of why you want to run this command.",
33
+ "description": "A brief explanation of why you want to run this command. Keep it to a single sentence.",
30
34
  },
31
35
  },
32
36
  "required": ["command", "explanation"],
@@ -84,6 +88,7 @@ READ_ONLY_COMMANDS = [
84
88
  "less",
85
89
  "more",
86
90
  "echo",
91
+ "head",
87
92
  "tail",
88
93
  "wc",
89
94
  "grep",
@@ -91,4 +96,5 @@ READ_ONLY_COMMANDS = [
91
96
  "ack",
92
97
  "which",
93
98
  "sed",
99
+ "find",
94
100
  ]
src/utils.py CHANGED
@@ -10,6 +10,11 @@ def is_read_only_command(command: str) -> bool:
10
10
  if ">" in command:
11
11
  return False
12
12
 
13
+ # Replace everything that potentially runs another command with a pipe
14
+ command = command.replace("&&", "|")
15
+ command = command.replace("||", "|")
16
+ command = command.replace(";", "|")
17
+
13
18
  pipes = command.split("|")
14
19
  for pipe in pipes:
15
20
  if pipe.strip().split()[0] not in READ_ONLY_COMMANDS:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tass
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A terminal assistant that allows you to ask an LLM to run commands.
5
5
  Project-URL: Homepage, https://github.com/cetincan0/tass
6
6
  Author: Can Cetin
@@ -1,14 +1,14 @@
1
1
  src/__init__.py,sha256=tu2q9W5_pkq30l3tRMTGahColBAAubbLP6LaB3l3IFg,89
2
- src/app.py,sha256=WO6KIHrQI8sL9IBLazeyiTaFp9UWG9n9GPiPxt9dRKs,6214
2
+ src/app.py,sha256=SG7jAfU49OtTTkuKSl7bIr-A5enrNLXZ0YZleTd7Qf0,8640
3
3
  src/cli.py,sha256=op3fYcyfek_KqCCiA-Zdlc9jVZSCi036whMmR2ZjjAs,76
4
- src/constants.py,sha256=qzqqLwVp6bVCbelvxGZlSYSnMKZFEjFvBWF-bW86ER8,3097
5
- src/utils.py,sha256=KLVKfTECfSCXqSkWmT9rK1wtjFvb6FJhusy5q7FGO_A,483
4
+ src/constants.py,sha256=Mhhm6JJII7NsTFhHVP0VA34gBKaXV00hkhWS0pSJJbk,3225
5
+ src/utils.py,sha256=rKq34DVmFbsWPy7R6Bfdvv1ztzFLPT4hUd8BFpPHjqs,681
6
6
  src/third_party/README.md,sha256=fjgAoAM_Vp1xOCdSYZT1menMom5dVDXa48h9VbDJfVI,160
7
7
  src/third_party/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  src/third_party/apply_patch.md,sha256=Q0bRVZepyrP0wF-IzNiNEfkXYcbFKJ42y7Hrl5Uxyfc,2578
9
9
  src/third_party/apply_patch.py,sha256=zZNWPp4bKuvHikGFQ_aHtDaWZ2ZH39c59J9Fb6NprCE,17575
10
- tass-0.1.0.dist-info/METADATA,sha256=oIKaTt006wFfm6a30O73pkQwM3dolzg_S4JgCaYNu3s,1071
11
- tass-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- tass-0.1.0.dist-info/entry_points.txt,sha256=pviKuIOuHvaQ7_YiFxatJEY8XYfh3EzVWy4LJh0v-A0,38
13
- tass-0.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
14
- tass-0.1.0.dist-info/RECORD,,
10
+ tass-0.1.2.dist-info/METADATA,sha256=vPQZSIRPdrFAlb4XNSGSj6gpF-k9fxdLmihDOstnDkU,1071
11
+ tass-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ tass-0.1.2.dist-info/entry_points.txt,sha256=pviKuIOuHvaQ7_YiFxatJEY8XYfh3EzVWy4LJh0v-A0,38
13
+ tass-0.1.2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
14
+ tass-0.1.2.dist-info/RECORD,,
File without changes