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 +95 -31
- src/constants.py +7 -1
- src/utils.py +5 -0
- {tass-0.1.0.dist-info → tass-0.1.2.dist-info}/METADATA +1 -1
- {tass-0.1.0.dist-info → tass-0.1.2.dist-info}/RECORD +8 -8
- {tass-0.1.0.dist-info → tass-0.1.2.dist-info}/WHEEL +0 -0
- {tass-0.1.0.dist-info → tass-0.1.2.dist-info}/entry_points.txt +0 -0
- {tass-0.1.0.dist-info → tass-0.1.2.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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}[/]
|
|
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
|
-
|
|
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
|
-
|
|
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,14 +1,14 @@
|
|
|
1
1
|
src/__init__.py,sha256=tu2q9W5_pkq30l3tRMTGahColBAAubbLP6LaB3l3IFg,89
|
|
2
|
-
src/app.py,sha256=
|
|
2
|
+
src/app.py,sha256=SG7jAfU49OtTTkuKSl7bIr-A5enrNLXZ0YZleTd7Qf0,8640
|
|
3
3
|
src/cli.py,sha256=op3fYcyfek_KqCCiA-Zdlc9jVZSCi036whMmR2ZjjAs,76
|
|
4
|
-
src/constants.py,sha256=
|
|
5
|
-
src/utils.py,sha256=
|
|
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.
|
|
11
|
-
tass-0.1.
|
|
12
|
-
tass-0.1.
|
|
13
|
-
tass-0.1.
|
|
14
|
-
tass-0.1.
|
|
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
|
|
File without changes
|
|
File without changes
|