tass 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- {tass-0.1.1 → tass-0.1.2}/PKG-INFO +1 -1
- {tass-0.1.1 → tass-0.1.2}/pyproject.toml +1 -1
- {tass-0.1.1 → tass-0.1.2}/src/app.py +93 -30
- {tass-0.1.1 → tass-0.1.2}/src/constants.py +7 -1
- {tass-0.1.1 → tass-0.1.2}/src/utils.py +5 -0
- {tass-0.1.1 → tass-0.1.2}/uv.lock +1 -1
- {tass-0.1.1 → tass-0.1.2}/.gitignore +0 -0
- {tass-0.1.1 → tass-0.1.2}/.python-version +0 -0
- {tass-0.1.1 → tass-0.1.2}/LICENSE +0 -0
- {tass-0.1.1 → tass-0.1.2}/README.md +0 -0
- {tass-0.1.1 → tass-0.1.2}/src/__init__.py +0 -0
- {tass-0.1.1 → tass-0.1.2}/src/cli.py +0 -0
- {tass-0.1.1 → tass-0.1.2}/src/third_party/README.md +0 -0
- {tass-0.1.1 → tass-0.1.2}/src/third_party/__init__.py +0 -0
- {tass-0.1.1 → tass-0.1.2}/src/third_party/apply_patch.md +0 -0
- {tass-0.1.1 → tass-0.1.2}/src/third_party/apply_patch.py +0 -0
- {tass-0.1.1 → tass-0.1.2}/tests/test_utils.py +0 -0
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
]
|
|
@@ -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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|