tass 0.1.1__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",
@@ -43,35 +93,36 @@ class TassApp:
43
93
 
44
94
  data = response.json()
45
95
  message = data["choices"][0]["message"]
46
- if message.get("tool_calls"):
47
- tool_name = message["tool_calls"][0]["function"]["name"]
48
- tool_args_str = message["tool_calls"][0]["function"]["arguments"]
49
- self.messages.append(
50
- {
51
- "role": "assistant",
52
- "tool_calls": [
53
- {
54
- "index": 0,
55
- "id": "id1",
56
- "type": "function",
57
- "function": {
58
- "name": tool_name,
59
- "arguments": tool_args_str
60
- }
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
61
112
  }
62
- ]
63
- }
64
- )
65
- try:
66
- tool = self.TOOLS_MAP[tool_name]
67
- tool_args = json.loads(tool_args_str)
68
- result = tool(**tool_args)
69
- self.messages.append({"role": "tool", "content": result})
70
- except Exception as e:
71
- self.messages.append({"role": "user", "content": str(e)})
72
- return self.call_llm()
73
-
74
- 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()
75
126
 
76
127
  def read_file(self, path: str, start: int = 1) -> str:
77
128
  console.print(f" └ Reading file [bold]{path}[/]...")
@@ -88,6 +139,7 @@ class TassApp:
88
139
  line_num += 1
89
140
 
90
141
  if len(lines) >= 1000:
142
+ lines.append("... (truncated)")
91
143
  break
92
144
 
93
145
  console.print(" [green]Command succeeded[/green]")
@@ -161,7 +213,11 @@ class TassApp:
161
213
  return f"Command output (exit {result.returncode}):\n{out}\n{err}"
162
214
 
163
215
  def run(self):
164
- console.print("Terminal Assistant")
216
+ try:
217
+ self._check_llm_host()
218
+ except KeyboardInterrupt:
219
+ console.print("\nBye!")
220
+ return
165
221
 
166
222
  while True:
167
223
  try:
@@ -180,8 +236,15 @@ class TassApp:
180
236
  self.messages.append({"role": "user", "content": user_input})
181
237
 
182
238
  while True:
183
- 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
+
184
245
  if llm_resp is not None:
185
246
  console.print("")
186
247
  console.print(Markdown(llm_resp))
248
+ self.messages.append({"role": "assistant", "content": llm_resp})
249
+ self.summarize()
187
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.1
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=8_nX46VV4avs20lqSRL__L0iu7igSiZkV7xjZ1A2fRY,6182
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.1.dist-info/METADATA,sha256=hAqKbUGBW_bjz308J-RqJrV3ydTxkx8hT9D7Y34gzQw,1071
11
- tass-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- tass-0.1.1.dist-info/entry_points.txt,sha256=pviKuIOuHvaQ7_YiFxatJEY8XYfh3EzVWy4LJh0v-A0,38
13
- tass-0.1.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
14
- tass-0.1.1.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