cortex-llm 1.0.9__py3-none-any.whl → 1.0.11__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.
- cortex/__init__.py +1 -1
- cortex/config.py +46 -10
- cortex/inference_engine.py +69 -32
- cortex/tools/__init__.py +5 -0
- cortex/tools/errors.py +9 -0
- cortex/tools/fs_ops.py +182 -0
- cortex/tools/protocol.py +76 -0
- cortex/tools/search.py +135 -0
- cortex/tools/tool_runner.py +204 -0
- cortex/ui/box_rendering.py +97 -0
- cortex/ui/cli.py +65 -1071
- cortex/ui/cli_commands.py +61 -0
- cortex/ui/cli_prompt.py +96 -0
- cortex/ui/help_ui.py +66 -0
- cortex/ui/input_box.py +205 -0
- cortex/ui/model_ui.py +408 -0
- cortex/ui/status_ui.py +78 -0
- cortex/ui/tool_activity.py +82 -0
- {cortex_llm-1.0.9.dist-info → cortex_llm-1.0.11.dist-info}/METADATA +3 -1
- {cortex_llm-1.0.9.dist-info → cortex_llm-1.0.11.dist-info}/RECORD +24 -10
- {cortex_llm-1.0.9.dist-info → cortex_llm-1.0.11.dist-info}/WHEEL +0 -0
- {cortex_llm-1.0.9.dist-info → cortex_llm-1.0.11.dist-info}/entry_points.txt +0 -0
- {cortex_llm-1.0.9.dist-info → cortex_llm-1.0.11.dist-info}/licenses/LICENSE +0 -0
- {cortex_llm-1.0.9.dist-info → cortex_llm-1.0.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Slash command parsing and dispatch for the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CommandHandlers:
|
|
11
|
+
show_help: Callable[[], None]
|
|
12
|
+
manage_models: Callable[[str], None]
|
|
13
|
+
download_model: Callable[[str], None]
|
|
14
|
+
clear_conversation: Callable[[], None]
|
|
15
|
+
save_conversation: Callable[[], None]
|
|
16
|
+
show_status: Callable[[], None]
|
|
17
|
+
show_gpu_status: Callable[[], None]
|
|
18
|
+
run_benchmark: Callable[[], None]
|
|
19
|
+
manage_template: Callable[[str], None]
|
|
20
|
+
run_finetune: Callable[[], None]
|
|
21
|
+
hf_login: Callable[[], None]
|
|
22
|
+
show_shortcuts: Callable[[], None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def handle_command(command: str, handlers: CommandHandlers) -> bool:
|
|
26
|
+
"""Handle slash commands. Returns False to exit."""
|
|
27
|
+
parts = command.split(maxsplit=1)
|
|
28
|
+
cmd = parts[0].lower()
|
|
29
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
30
|
+
|
|
31
|
+
if cmd == "/help":
|
|
32
|
+
handlers.show_help()
|
|
33
|
+
elif cmd == "/model":
|
|
34
|
+
handlers.manage_models(args)
|
|
35
|
+
elif cmd == "/download":
|
|
36
|
+
handlers.download_model(args)
|
|
37
|
+
elif cmd == "/clear":
|
|
38
|
+
handlers.clear_conversation()
|
|
39
|
+
elif cmd == "/save":
|
|
40
|
+
handlers.save_conversation()
|
|
41
|
+
elif cmd == "/status":
|
|
42
|
+
handlers.show_status()
|
|
43
|
+
elif cmd == "/gpu":
|
|
44
|
+
handlers.show_gpu_status()
|
|
45
|
+
elif cmd == "/benchmark":
|
|
46
|
+
handlers.run_benchmark()
|
|
47
|
+
elif cmd == "/template":
|
|
48
|
+
handlers.manage_template(args)
|
|
49
|
+
elif cmd == "/finetune":
|
|
50
|
+
handlers.run_finetune()
|
|
51
|
+
elif cmd == "/login":
|
|
52
|
+
handlers.hf_login()
|
|
53
|
+
elif cmd in ["/quit", "/exit"]:
|
|
54
|
+
return False
|
|
55
|
+
elif cmd == "?":
|
|
56
|
+
handlers.show_shortcuts()
|
|
57
|
+
else:
|
|
58
|
+
print(f"\033[31mUnknown command: {cmd}\033[0m")
|
|
59
|
+
print("\033[2mType /help for available commands\033[0m")
|
|
60
|
+
|
|
61
|
+
return True
|
cortex/ui/cli_prompt.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Prompt formatting helpers for chat templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cortex.conversation_manager import MessageRole
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_prompt_with_chat_template(
|
|
9
|
+
*,
|
|
10
|
+
conversation_manager,
|
|
11
|
+
model_manager,
|
|
12
|
+
template_registry,
|
|
13
|
+
user_input: str,
|
|
14
|
+
include_user: bool = True,
|
|
15
|
+
logger=None,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Format the prompt with the appropriate chat template for the model."""
|
|
18
|
+
# Get current conversation context
|
|
19
|
+
conversation = conversation_manager.get_current_conversation()
|
|
20
|
+
|
|
21
|
+
# Get the tokenizer for the current model
|
|
22
|
+
model_name = model_manager.current_model
|
|
23
|
+
tokenizer = model_manager.tokenizers.get(model_name)
|
|
24
|
+
|
|
25
|
+
# Build messages list from conversation history
|
|
26
|
+
messages = []
|
|
27
|
+
if conversation and conversation.messages:
|
|
28
|
+
context_messages = conversation.messages[-10:]
|
|
29
|
+
for msg in context_messages:
|
|
30
|
+
messages.append({
|
|
31
|
+
"role": msg.role.value,
|
|
32
|
+
"content": msg.content
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
# Add current user message
|
|
36
|
+
if include_user:
|
|
37
|
+
messages.append({
|
|
38
|
+
"role": "user",
|
|
39
|
+
"content": user_input
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
# Use template registry to format messages
|
|
43
|
+
try:
|
|
44
|
+
profile = template_registry.setup_model(
|
|
45
|
+
model_name,
|
|
46
|
+
tokenizer=tokenizer,
|
|
47
|
+
interactive=False
|
|
48
|
+
)
|
|
49
|
+
formatted = profile.format_messages(messages, add_generation_prompt=True)
|
|
50
|
+
return formatted
|
|
51
|
+
|
|
52
|
+
except (AttributeError, TypeError, ValueError) as e:
|
|
53
|
+
if logger is not None:
|
|
54
|
+
logger.debug(f"Template registry failed: {e}, using fallback")
|
|
55
|
+
|
|
56
|
+
if tokenizer and hasattr(tokenizer, 'apply_chat_template'):
|
|
57
|
+
try:
|
|
58
|
+
formatted = tokenizer.apply_chat_template(
|
|
59
|
+
messages,
|
|
60
|
+
tokenize=False,
|
|
61
|
+
add_generation_prompt=True
|
|
62
|
+
)
|
|
63
|
+
return formatted
|
|
64
|
+
except (AttributeError, TypeError, ValueError) as e:
|
|
65
|
+
if logger is not None:
|
|
66
|
+
logger.debug(f"Tokenizer apply_chat_template failed: {e}")
|
|
67
|
+
|
|
68
|
+
# Fallback: For TinyLlama and other chat models, use the proper format
|
|
69
|
+
if model_name and "chat" in model_name.lower():
|
|
70
|
+
history = ""
|
|
71
|
+
if conversation and conversation.messages:
|
|
72
|
+
recent_messages = conversation.messages[-6:]
|
|
73
|
+
for msg in recent_messages:
|
|
74
|
+
if msg.role == MessageRole.USER:
|
|
75
|
+
history += f"<|user|>\n{msg.content}</s>\n"
|
|
76
|
+
elif msg.role == MessageRole.ASSISTANT:
|
|
77
|
+
history += f"<|assistant|>\n{msg.content}</s>\n"
|
|
78
|
+
|
|
79
|
+
prompt = f"{history}<|user|>\n{user_input}</s>\n<|assistant|>\n"
|
|
80
|
+
return prompt
|
|
81
|
+
|
|
82
|
+
# Generic fallback for non-chat models
|
|
83
|
+
if conversation and len(conversation.messages) > 0:
|
|
84
|
+
context = ""
|
|
85
|
+
recent_messages = conversation.messages[-6:]
|
|
86
|
+
for msg in recent_messages:
|
|
87
|
+
if msg.role == MessageRole.USER:
|
|
88
|
+
context += f"User: {msg.content}\n"
|
|
89
|
+
elif msg.role == MessageRole.ASSISTANT:
|
|
90
|
+
context += f"Assistant: {msg.content}\n"
|
|
91
|
+
|
|
92
|
+
prompt = f"{context}User: {user_input}\nAssistant:"
|
|
93
|
+
else:
|
|
94
|
+
prompt = f"User: {user_input}\nAssistant:"
|
|
95
|
+
|
|
96
|
+
return prompt
|
cortex/ui/help_ui.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Help and shortcuts rendering for CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def show_shortcuts(*, terminal_width: int, box: Any) -> None:
|
|
9
|
+
"""Show keyboard shortcuts."""
|
|
10
|
+
width = min(terminal_width - 2, 70)
|
|
11
|
+
|
|
12
|
+
print()
|
|
13
|
+
box.print_box_header("Keyboard Shortcuts", width)
|
|
14
|
+
box.print_empty_line(width)
|
|
15
|
+
|
|
16
|
+
shortcuts = [
|
|
17
|
+
("Ctrl+C", "Cancel current generation"),
|
|
18
|
+
("Ctrl+D", "Exit Cortex"),
|
|
19
|
+
("Tab", "Auto-complete commands"),
|
|
20
|
+
("/help", "Show all commands"),
|
|
21
|
+
("?", "Show this help"),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
for key, desc in shortcuts:
|
|
25
|
+
colored_key = f"\033[93m{key}\033[0m"
|
|
26
|
+
key_width = len(key)
|
|
27
|
+
padding = " " * (12 - key_width)
|
|
28
|
+
line = f" {colored_key}{padding}{desc}"
|
|
29
|
+
box.print_box_line(line, width)
|
|
30
|
+
|
|
31
|
+
box.print_empty_line(width)
|
|
32
|
+
box.print_box_footer(width)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def show_help(*, terminal_width: int, box: Any) -> None:
|
|
36
|
+
"""Show available commands."""
|
|
37
|
+
width = min(terminal_width - 2, 70)
|
|
38
|
+
|
|
39
|
+
print()
|
|
40
|
+
box.print_box_header("Available Commands", width)
|
|
41
|
+
box.print_empty_line(width)
|
|
42
|
+
|
|
43
|
+
commands = [
|
|
44
|
+
("/help", "Show this help message"),
|
|
45
|
+
("/status", "Show current setup and GPU info"),
|
|
46
|
+
("/download", "Download a model from HuggingFace"),
|
|
47
|
+
("/model", "Manage models (load/delete/info)"),
|
|
48
|
+
("/finetune", "Fine-tune a model interactively"),
|
|
49
|
+
("/clear", "Clear conversation history"),
|
|
50
|
+
("/save", "Save current conversation"),
|
|
51
|
+
("/template", "Manage chat templates"),
|
|
52
|
+
("/gpu", "Show GPU status"),
|
|
53
|
+
("/benchmark", "Run performance benchmark"),
|
|
54
|
+
("/login", "Login to HuggingFace for gated models"),
|
|
55
|
+
("/quit", "Exit Cortex"),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
for cmd, desc in commands:
|
|
59
|
+
colored_cmd = f"\033[93m{cmd}\033[0m"
|
|
60
|
+
cmd_width = len(cmd)
|
|
61
|
+
padding = " " * (12 - cmd_width)
|
|
62
|
+
line = f" {colored_cmd}{padding}{desc}"
|
|
63
|
+
box.print_box_line(line, width)
|
|
64
|
+
|
|
65
|
+
box.print_empty_line(width)
|
|
66
|
+
box.print_box_footer(width)
|
cortex/ui/input_box.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Input box rendering and protected input handling for CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import termios
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
INPUT_BG = "\033[48;5;236m" # Dark gray background (256-color)
|
|
13
|
+
INPUT_FG = "\033[30m" # Black text
|
|
14
|
+
RESET = "\033[0m"
|
|
15
|
+
PROMPT_PREFIX = " > "
|
|
16
|
+
BOX_HEIGHT = 3
|
|
17
|
+
INPUT_LINE_OFFSET = BOX_HEIGHT // 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def prompt_input_box(
|
|
21
|
+
*,
|
|
22
|
+
console: Console,
|
|
23
|
+
terminal_width: int,
|
|
24
|
+
current_model_path: Optional[str],
|
|
25
|
+
) -> str:
|
|
26
|
+
"""Render the solid input box, read user input, and clean up the UI."""
|
|
27
|
+
width = terminal_width
|
|
28
|
+
|
|
29
|
+
# ANSI codes
|
|
30
|
+
yellow = "\033[93m"
|
|
31
|
+
dim = "\033[2m"
|
|
32
|
+
clear_line = "\033[2K"
|
|
33
|
+
cursor_up = "\033[A"
|
|
34
|
+
cursor_down = "\033[B"
|
|
35
|
+
|
|
36
|
+
# Model name line
|
|
37
|
+
current_model = ""
|
|
38
|
+
if current_model_path:
|
|
39
|
+
model_name = os.path.basename(current_model_path)
|
|
40
|
+
current_model = f"{dim}Model:{RESET} {yellow}{model_name}{RESET}"
|
|
41
|
+
|
|
42
|
+
# Draw the input box with a solid background (no borders)
|
|
43
|
+
print()
|
|
44
|
+
fill_line = f"{INPUT_BG}{' ' * width}{RESET}"
|
|
45
|
+
for _ in range(BOX_HEIGHT):
|
|
46
|
+
print(fill_line)
|
|
47
|
+
|
|
48
|
+
# Bottom hint: show current model aligned with box
|
|
49
|
+
if current_model:
|
|
50
|
+
print(f"{current_model}")
|
|
51
|
+
else:
|
|
52
|
+
print()
|
|
53
|
+
|
|
54
|
+
# Move cursor to input position inside the box (center line)
|
|
55
|
+
move_up = BOX_HEIGHT - INPUT_LINE_OFFSET + 1
|
|
56
|
+
sys.stdout.write(f"\033[{move_up}A")
|
|
57
|
+
sys.stdout.write(f"\r{INPUT_BG}{INPUT_FG}{PROMPT_PREFIX}")
|
|
58
|
+
sys.stdout.flush()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
user_input = _get_protected_input(width)
|
|
62
|
+
|
|
63
|
+
# Clear the input box region using relative moves.
|
|
64
|
+
sys.stdout.write(f"{cursor_down}\r{clear_line}")
|
|
65
|
+
for _ in range(INPUT_LINE_OFFSET + 3):
|
|
66
|
+
sys.stdout.write(f"{cursor_up}\r{clear_line}")
|
|
67
|
+
|
|
68
|
+
# Print the clean prompt that represents the submitted user message.
|
|
69
|
+
sys.stdout.write("\r> " + user_input.strip() + "\n")
|
|
70
|
+
sys.stdout.flush()
|
|
71
|
+
|
|
72
|
+
return user_input.strip()
|
|
73
|
+
|
|
74
|
+
except KeyboardInterrupt:
|
|
75
|
+
raise
|
|
76
|
+
except EOFError:
|
|
77
|
+
try:
|
|
78
|
+
sys.stdout.write(f"\r{clear_line}")
|
|
79
|
+
for _ in range(BOX_HEIGHT - INPUT_LINE_OFFSET):
|
|
80
|
+
sys.stdout.write(f"{cursor_down}\r{clear_line}")
|
|
81
|
+
for _ in range(BOX_HEIGHT):
|
|
82
|
+
sys.stdout.write(f"{cursor_up}\r{clear_line}")
|
|
83
|
+
sys.stdout.flush()
|
|
84
|
+
finally:
|
|
85
|
+
pass
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_protected_input(box_width: int) -> str:
|
|
90
|
+
"""Read input in raw mode and prevent deleting the prompt."""
|
|
91
|
+
# Calculate usable width for text (leave one trailing space to avoid wrap)
|
|
92
|
+
max_display_width = box_width - len(PROMPT_PREFIX) - 1
|
|
93
|
+
clear_line = "\033[2K"
|
|
94
|
+
cursor_down = "\033[1B"
|
|
95
|
+
cursor_up = "\033[1A"
|
|
96
|
+
|
|
97
|
+
# Store terminal settings
|
|
98
|
+
old_settings = termios.tcgetattr(sys.stdin)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Set terminal to raw mode for character-by-character input
|
|
102
|
+
# Disable ISIG so we can handle Ctrl+C manually for clean exit
|
|
103
|
+
new_settings = termios.tcgetattr(sys.stdin)
|
|
104
|
+
new_settings[3] = new_settings[3] & ~termios.ICANON
|
|
105
|
+
new_settings[3] = new_settings[3] & ~termios.ECHO
|
|
106
|
+
new_settings[3] = new_settings[3] & ~termios.ISIG
|
|
107
|
+
new_settings[6][termios.VMIN] = 1
|
|
108
|
+
new_settings[6][termios.VTIME] = 0
|
|
109
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
|
|
110
|
+
|
|
111
|
+
input_buffer = []
|
|
112
|
+
cursor_pos = 0
|
|
113
|
+
view_offset = 0
|
|
114
|
+
|
|
115
|
+
def redraw_line():
|
|
116
|
+
nonlocal view_offset
|
|
117
|
+
|
|
118
|
+
if len(input_buffer) <= max_display_width:
|
|
119
|
+
display_text = "".join(input_buffer)
|
|
120
|
+
display_cursor_pos = cursor_pos
|
|
121
|
+
else:
|
|
122
|
+
if cursor_pos < view_offset:
|
|
123
|
+
view_offset = cursor_pos
|
|
124
|
+
elif cursor_pos >= view_offset + max_display_width:
|
|
125
|
+
view_offset = cursor_pos - max_display_width + 1
|
|
126
|
+
|
|
127
|
+
display_text = "".join(input_buffer[view_offset:view_offset + max_display_width])
|
|
128
|
+
display_cursor_pos = cursor_pos - view_offset
|
|
129
|
+
|
|
130
|
+
pad_len = box_width - len(PROMPT_PREFIX) - len(display_text)
|
|
131
|
+
if pad_len < 0:
|
|
132
|
+
pad_len = 0
|
|
133
|
+
|
|
134
|
+
sys.stdout.write(
|
|
135
|
+
f"\r{INPUT_BG}{INPUT_FG}{PROMPT_PREFIX}{display_text}"
|
|
136
|
+
f"{' ' * pad_len}{RESET}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
cursor_column = len(PROMPT_PREFIX) + 1 + display_cursor_pos
|
|
140
|
+
sys.stdout.write(f"\033[{cursor_column}G")
|
|
141
|
+
sys.stdout.flush()
|
|
142
|
+
|
|
143
|
+
redraw_line()
|
|
144
|
+
|
|
145
|
+
def clear_box_from_input():
|
|
146
|
+
sys.stdout.write(f"\r{clear_line}")
|
|
147
|
+
for _ in range(BOX_HEIGHT - INPUT_LINE_OFFSET):
|
|
148
|
+
sys.stdout.write(f"{cursor_down}\r{clear_line}")
|
|
149
|
+
for _ in range(BOX_HEIGHT):
|
|
150
|
+
sys.stdout.write(f"{cursor_up}\r{clear_line}")
|
|
151
|
+
sys.stdout.write("\r")
|
|
152
|
+
sys.stdout.flush()
|
|
153
|
+
|
|
154
|
+
while True:
|
|
155
|
+
char = sys.stdin.read(1)
|
|
156
|
+
|
|
157
|
+
if char == "\r" or char == "\n":
|
|
158
|
+
sys.stdout.write("\r\n")
|
|
159
|
+
sys.stdout.write("\r\n")
|
|
160
|
+
sys.stdout.flush()
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
if char == "\x7f" or char == "\x08":
|
|
164
|
+
if cursor_pos > 0:
|
|
165
|
+
cursor_pos -= 1
|
|
166
|
+
input_buffer.pop(cursor_pos)
|
|
167
|
+
redraw_line()
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if char == "\x03":
|
|
171
|
+
clear_box_from_input()
|
|
172
|
+
raise KeyboardInterrupt
|
|
173
|
+
|
|
174
|
+
if char == "\x04":
|
|
175
|
+
raise EOFError
|
|
176
|
+
|
|
177
|
+
if char == "\x1b":
|
|
178
|
+
next1 = sys.stdin.read(1)
|
|
179
|
+
if next1 == "[":
|
|
180
|
+
next2 = sys.stdin.read(1)
|
|
181
|
+
if next2 == "D":
|
|
182
|
+
if cursor_pos > 0:
|
|
183
|
+
cursor_pos -= 1
|
|
184
|
+
redraw_line()
|
|
185
|
+
elif next2 == "C":
|
|
186
|
+
if cursor_pos < len(input_buffer):
|
|
187
|
+
cursor_pos += 1
|
|
188
|
+
redraw_line()
|
|
189
|
+
elif next2 == "H":
|
|
190
|
+
cursor_pos = 0
|
|
191
|
+
view_offset = 0
|
|
192
|
+
redraw_line()
|
|
193
|
+
elif next2 == "F":
|
|
194
|
+
cursor_pos = len(input_buffer)
|
|
195
|
+
redraw_line()
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if ord(char) >= 32:
|
|
199
|
+
input_buffer.insert(cursor_pos, char)
|
|
200
|
+
cursor_pos += 1
|
|
201
|
+
redraw_line()
|
|
202
|
+
|
|
203
|
+
return "".join(input_buffer)
|
|
204
|
+
finally:
|
|
205
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|