tzamuncode 0.1.0__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.
- tzamuncode/__init__.py +10 -0
- tzamuncode/agents/__init__.py +1 -0
- tzamuncode/agents/coder.py +144 -0
- tzamuncode/agents/tools.py +159 -0
- tzamuncode/auth/__init__.py +1 -0
- tzamuncode/auth/auth_manager.py +159 -0
- tzamuncode/cli/__init__.py +1 -0
- tzamuncode/cli/agentic_commands.py +131 -0
- tzamuncode/cli/auth_commands.py +125 -0
- tzamuncode/cli/commands.py +203 -0
- tzamuncode/cli/enhanced_chat.py +312 -0
- tzamuncode/cli/interactive_chat.py +323 -0
- tzamuncode/cli/main.py +444 -0
- tzamuncode/cli/realtime_chat.py +965 -0
- tzamuncode/cli/realtime_chat_methods.py +200 -0
- tzamuncode/cli/tui_chat.py +323 -0
- tzamuncode/config/__init__.py +1 -0
- tzamuncode/models/__init__.py +1 -0
- tzamuncode/models/ollama.py +124 -0
- tzamuncode/models/vllm_client.py +121 -0
- tzamuncode/utils/__init__.py +1 -0
- tzamuncode/utils/file_ops.py +59 -0
- tzamuncode/utils/project_scanner.py +193 -0
- tzamuncode-0.1.0.dist-info/METADATA +200 -0
- tzamuncode-0.1.0.dist-info/RECORD +29 -0
- tzamuncode-0.1.0.dist-info/WHEEL +5 -0
- tzamuncode-0.1.0.dist-info/entry_points.txt +3 -0
- tzamuncode-0.1.0.dist-info/licenses/LICENSE +21 -0
- tzamuncode-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Additional methods for RealtimeChat class
|
|
3
|
+
These will be added to realtime_chat.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from rich.syntax import Syntax
|
|
9
|
+
from rich.prompt import Prompt, Confirm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_file(self, file_path: str):
|
|
13
|
+
"""Read and display a file with syntax highlighting"""
|
|
14
|
+
try:
|
|
15
|
+
full_path = self.workspace / file_path
|
|
16
|
+
|
|
17
|
+
if not full_path.exists():
|
|
18
|
+
console.print(f"\n[bold red]✗[/bold red] File not found: {file_path}\n")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
if full_path.is_dir():
|
|
22
|
+
console.print(f"\n[bold red]✗[/bold red] {file_path} is a directory, not a file\n")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# Read file content
|
|
26
|
+
content = full_path.read_text()
|
|
27
|
+
|
|
28
|
+
# Detect language for syntax highlighting
|
|
29
|
+
suffix = full_path.suffix.lstrip('.')
|
|
30
|
+
lang_map = {
|
|
31
|
+
'py': 'python', 'js': 'javascript', 'ts': 'typescript',
|
|
32
|
+
'jsx': 'jsx', 'tsx': 'tsx', 'java': 'java', 'cpp': 'cpp',
|
|
33
|
+
'c': 'c', 'go': 'go', 'rs': 'rust', 'rb': 'ruby',
|
|
34
|
+
'php': 'php', 'html': 'html', 'css': 'css', 'json': 'json',
|
|
35
|
+
'yaml': 'yaml', 'yml': 'yaml', 'xml': 'xml', 'md': 'markdown',
|
|
36
|
+
'sh': 'bash', 'bash': 'bash', 'sql': 'sql'
|
|
37
|
+
}
|
|
38
|
+
language = lang_map.get(suffix, 'text')
|
|
39
|
+
|
|
40
|
+
console.print()
|
|
41
|
+
console.print(f"[bold cyan]📄 {file_path}[/bold cyan] ({len(content)} chars, {len(content.splitlines())} lines)\n")
|
|
42
|
+
|
|
43
|
+
# Syntax highlighted display
|
|
44
|
+
syntax = Syntax(content, language, theme="monokai", line_numbers=True)
|
|
45
|
+
console.print(syntax)
|
|
46
|
+
console.print()
|
|
47
|
+
|
|
48
|
+
# Add to conversation context
|
|
49
|
+
self.conversation_history.append({
|
|
50
|
+
"role": "user",
|
|
51
|
+
"content": f"I read the file {file_path}:\n```{language}\n{content}\n```"
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
console.print(f"\n[bold red]✗ Error reading file:[/bold red] {e}\n")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write_file(self, file_path: str):
|
|
59
|
+
"""Write content to a file interactively"""
|
|
60
|
+
try:
|
|
61
|
+
console.print()
|
|
62
|
+
console.print(f"[bold cyan]Writing to:[/bold cyan] {file_path}")
|
|
63
|
+
console.print("[dim]Enter content (type 'EOF' on a new line to finish):[/dim]\n")
|
|
64
|
+
|
|
65
|
+
lines = []
|
|
66
|
+
while True:
|
|
67
|
+
line = input()
|
|
68
|
+
if line == 'EOF':
|
|
69
|
+
break
|
|
70
|
+
lines.append(line)
|
|
71
|
+
|
|
72
|
+
content = '\n'.join(lines)
|
|
73
|
+
|
|
74
|
+
full_path = self.workspace / file_path
|
|
75
|
+
|
|
76
|
+
# Confirm if file exists
|
|
77
|
+
if full_path.exists():
|
|
78
|
+
if not Confirm.ask(f"\n[bold yellow]⚠[/bold yellow] File exists. Overwrite?"):
|
|
79
|
+
console.print("[dim]Write cancelled[/dim]\n")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Create parent directories if needed
|
|
83
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
|
|
85
|
+
# Write file
|
|
86
|
+
full_path.write_text(content)
|
|
87
|
+
|
|
88
|
+
console.print(f"\n[bold green]✓[/bold green] Written {len(content)} chars to {file_path}\n")
|
|
89
|
+
|
|
90
|
+
# Add to conversation context
|
|
91
|
+
self.conversation_history.append({
|
|
92
|
+
"role": "user",
|
|
93
|
+
"content": f"I wrote to file {file_path}:\n```\n{content}\n```"
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
except KeyboardInterrupt:
|
|
97
|
+
console.print("\n[dim]Write cancelled[/dim]\n")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
console.print(f"\n[bold red]✗ Error writing file:[/bold red] {e}\n")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def switch_backend(self):
|
|
103
|
+
"""Switch between vLLM and Ollama backends"""
|
|
104
|
+
console.print()
|
|
105
|
+
console.print("[bold cyan]Switch AI Backend[/bold cyan]\n")
|
|
106
|
+
console.print(f"Current: [bold]{'vLLM' if self.use_vllm else 'Ollama'}[/bold]\n")
|
|
107
|
+
console.print(" [bold]1[/bold]. ⚡ [cyan]vLLM[/cyan] - Fast inference (Qwen 2.5 7B)")
|
|
108
|
+
console.print(" [bold]2[/bold]. 🦙 [green]Ollama[/green] - Powerful models (Qwen 2.5 32B)")
|
|
109
|
+
|
|
110
|
+
choice = Prompt.ask("\nSwitch to", choices=["1", "2"], default="2" if self.use_vllm else "1")
|
|
111
|
+
|
|
112
|
+
new_use_vllm = (choice == "1")
|
|
113
|
+
|
|
114
|
+
if new_use_vllm == self.use_vllm:
|
|
115
|
+
console.print("\n[dim]Already using this backend[/dim]\n")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Switch backend
|
|
119
|
+
self.use_vllm = new_use_vllm
|
|
120
|
+
|
|
121
|
+
if self.use_vllm:
|
|
122
|
+
from ..models.vllm_client import VLLMClient
|
|
123
|
+
self.model = "qwen2.5-7b-instruct"
|
|
124
|
+
self.client = VLLMClient(model=self.model)
|
|
125
|
+
self.backend = "vLLM"
|
|
126
|
+
console.print("\n[bold green]✓[/bold green] Switched to [cyan]vLLM[/cyan] backend\n")
|
|
127
|
+
else:
|
|
128
|
+
from ..models.ollama import OllamaClient
|
|
129
|
+
self.model = "qwen2.5:32b"
|
|
130
|
+
self.client = OllamaClient(model=self.model)
|
|
131
|
+
self.backend = "Ollama"
|
|
132
|
+
console.print("\n[bold green]✓[/bold green] Switched to [green]Ollama[/green] backend\n")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def show_conversation_history(self):
|
|
136
|
+
"""Show conversation history"""
|
|
137
|
+
console.print()
|
|
138
|
+
|
|
139
|
+
if len(self.conversation_history) <= 1: # Only system prompt
|
|
140
|
+
console.print("[dim]No conversation history yet[/dim]\n")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
console.print(f"[bold cyan]Conversation History[/bold cyan] ({len(self.conversation_history) - 1} messages)\n")
|
|
144
|
+
|
|
145
|
+
for idx, msg in enumerate(self.conversation_history[1:], 1): # Skip system prompt
|
|
146
|
+
role = msg['role']
|
|
147
|
+
content = msg['content']
|
|
148
|
+
|
|
149
|
+
if role == 'user':
|
|
150
|
+
console.print(f"[bold green]{idx}. You[/bold green] › {content[:100]}{'...' if len(content) > 100 else ''}")
|
|
151
|
+
else:
|
|
152
|
+
console.print(f"[bold blue]{idx}. AI[/bold blue] › {content[:100]}{'...' if len(content) > 100 else ''}")
|
|
153
|
+
|
|
154
|
+
console.print()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def save_conversation(self, filename: str):
|
|
158
|
+
"""Save conversation to JSON file"""
|
|
159
|
+
try:
|
|
160
|
+
save_path = self.workspace / filename
|
|
161
|
+
|
|
162
|
+
# Prepare data
|
|
163
|
+
data = {
|
|
164
|
+
'model': self.model,
|
|
165
|
+
'backend': self.backend,
|
|
166
|
+
'workspace': str(self.workspace),
|
|
167
|
+
'conversation': self.conversation_history
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Save to file
|
|
171
|
+
with open(save_path, 'w') as f:
|
|
172
|
+
json.dump(data, f, indent=2)
|
|
173
|
+
|
|
174
|
+
console.print(f"\n[bold green]✓[/bold green] Conversation saved to {filename}\n")
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
console.print(f"\n[bold red]✗ Error saving:[/bold red] {e}\n")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def load_conversation(self, filename: str):
|
|
181
|
+
"""Load conversation from JSON file"""
|
|
182
|
+
try:
|
|
183
|
+
load_path = self.workspace / filename
|
|
184
|
+
|
|
185
|
+
if not load_path.exists():
|
|
186
|
+
console.print(f"\n[bold red]✗[/bold red] File not found: {filename}\n")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Load data
|
|
190
|
+
with open(load_path, 'r') as f:
|
|
191
|
+
data = json.load(f)
|
|
192
|
+
|
|
193
|
+
# Restore conversation
|
|
194
|
+
self.conversation_history = data['conversation']
|
|
195
|
+
|
|
196
|
+
console.print(f"\n[bold green]✓[/bold green] Loaded conversation from {filename}")
|
|
197
|
+
console.print(f"[dim]Model: {data.get('model', 'unknown')} | Messages: {len(self.conversation_history)}[/dim]\n")
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
console.print(f"\n[bold red]✗ Error loading:[/bold red] {e}\n")
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TUI Chat Interface for TzamunCode
|
|
3
|
+
Built with Textual for a Claude Code-like experience
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from textual.app import App, ComposeResult
|
|
7
|
+
from textual.containers import Container, Vertical, Horizontal
|
|
8
|
+
from textual.widgets import Header, Footer, Static, Input, RichLog
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual import events
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from ..models.ollama import OllamaClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CommandMenu(Static):
|
|
20
|
+
"""Popup menu for slash commands"""
|
|
21
|
+
|
|
22
|
+
COMMANDS = {
|
|
23
|
+
'/help': 'Show available commands',
|
|
24
|
+
'/models': 'List and switch models',
|
|
25
|
+
'/settings': 'Show settings',
|
|
26
|
+
'/clear': 'Clear conversation history',
|
|
27
|
+
'/exit': 'Exit chat',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def compose(self) -> ComposeResult:
|
|
31
|
+
table = Table(title="Available Commands", border_style="cyan")
|
|
32
|
+
table.add_column("Command", style="cyan", no_wrap=True)
|
|
33
|
+
table.add_column("Description", style="white")
|
|
34
|
+
|
|
35
|
+
for cmd, desc in self.COMMANDS.items():
|
|
36
|
+
table.add_row(cmd, desc)
|
|
37
|
+
|
|
38
|
+
yield Static(table)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ShortcutsMenu(Static):
|
|
42
|
+
"""Popup menu for keyboard shortcuts"""
|
|
43
|
+
|
|
44
|
+
def compose(self) -> ComposeResult:
|
|
45
|
+
table = Table(title="Keyboard Shortcuts", border_style="blue")
|
|
46
|
+
table.add_column("Key", style="cyan", no_wrap=True)
|
|
47
|
+
table.add_column("Action", style="white")
|
|
48
|
+
|
|
49
|
+
table.add_row("Ctrl+C", "Exit chat")
|
|
50
|
+
table.add_row("Ctrl+L", "Clear screen")
|
|
51
|
+
table.add_row("/", "Show commands")
|
|
52
|
+
table.add_row("?", "Show this help")
|
|
53
|
+
|
|
54
|
+
yield Static(table)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ChatMessage(Static):
|
|
58
|
+
"""A single chat message"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, role: str, content: str, **kwargs):
|
|
61
|
+
super().__init__(**kwargs)
|
|
62
|
+
self.role = role
|
|
63
|
+
self.content = content
|
|
64
|
+
|
|
65
|
+
def render(self) -> Text:
|
|
66
|
+
if self.role == "user":
|
|
67
|
+
return Text(f"You › {self.content}", style="bold green")
|
|
68
|
+
else:
|
|
69
|
+
return Text(f"TzamunCode › {self.content}", style="bold blue")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TUIChat(App):
|
|
73
|
+
"""TUI Chat Application"""
|
|
74
|
+
|
|
75
|
+
CSS = """
|
|
76
|
+
Screen {
|
|
77
|
+
background: $surface;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#header {
|
|
81
|
+
dock: top;
|
|
82
|
+
height: 5;
|
|
83
|
+
background: $primary;
|
|
84
|
+
color: $text;
|
|
85
|
+
content-align: center middle;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#messages {
|
|
89
|
+
height: 1fr;
|
|
90
|
+
border: solid $primary;
|
|
91
|
+
background: $surface;
|
|
92
|
+
overflow-y: scroll;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#input-container {
|
|
96
|
+
dock: bottom;
|
|
97
|
+
height: 3;
|
|
98
|
+
background: $panel;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#input {
|
|
102
|
+
width: 100%;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.command-menu {
|
|
106
|
+
layer: overlay;
|
|
107
|
+
offset: 0 -10;
|
|
108
|
+
width: 60;
|
|
109
|
+
height: auto;
|
|
110
|
+
background: $panel;
|
|
111
|
+
border: solid $primary;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.shortcuts-menu {
|
|
115
|
+
layer: overlay;
|
|
116
|
+
offset: 0 -10;
|
|
117
|
+
width: 40;
|
|
118
|
+
height: auto;
|
|
119
|
+
background: $panel;
|
|
120
|
+
border: solid $primary;
|
|
121
|
+
}
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
BINDINGS = [
|
|
125
|
+
Binding("ctrl+c", "quit", "Quit"),
|
|
126
|
+
Binding("ctrl+l", "clear", "Clear"),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
def __init__(self, model: str = "qwen2.5:32b", **kwargs):
|
|
130
|
+
super().__init__(**kwargs)
|
|
131
|
+
self.model = model
|
|
132
|
+
self.client = OllamaClient(model=model)
|
|
133
|
+
self.conversation_history = []
|
|
134
|
+
self.command_menu_visible = False
|
|
135
|
+
self.shortcuts_menu_visible = False
|
|
136
|
+
|
|
137
|
+
def compose(self) -> ComposeResult:
|
|
138
|
+
"""Create child widgets"""
|
|
139
|
+
# Header with ASCII logo
|
|
140
|
+
header_text = """╔╦╗╔═╗╔═╗╔╦╗╦ ╦╔╗╔╔═╗╔═╗╔╦╗╔═╗
|
|
141
|
+
║ ╔═╝╠═╣║║║║ ║║║║║ ║ ║ ║║╣
|
|
142
|
+
╩ ╚═╝╩ ╩╩ ╩╚═╝╝╚╝╚═╝╚═╝═╩╝╚═╝
|
|
143
|
+
AI Coding Assistant • Model: """ + self.model
|
|
144
|
+
|
|
145
|
+
yield Static(header_text, id="header")
|
|
146
|
+
|
|
147
|
+
# Message history
|
|
148
|
+
yield RichLog(id="messages", highlight=True, markup=True)
|
|
149
|
+
|
|
150
|
+
# Input container
|
|
151
|
+
with Container(id="input-container"):
|
|
152
|
+
yield Input(placeholder="Type your message... (/ for commands, ? for shortcuts)", id="input")
|
|
153
|
+
|
|
154
|
+
def on_mount(self) -> None:
|
|
155
|
+
"""Called when app starts"""
|
|
156
|
+
messages = self.query_one("#messages", RichLog)
|
|
157
|
+
messages.write("[bold green]Welcome to TzamunCode![/bold green]")
|
|
158
|
+
messages.write("[dim]Type '/' for commands or '?' for shortcuts[/dim]")
|
|
159
|
+
messages.write("")
|
|
160
|
+
|
|
161
|
+
# Focus input
|
|
162
|
+
self.query_one("#input", Input).focus()
|
|
163
|
+
|
|
164
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
165
|
+
"""Handle input changes in real-time"""
|
|
166
|
+
current_value = event.value
|
|
167
|
+
|
|
168
|
+
# Show command menu when user types '/'
|
|
169
|
+
if current_value.endswith('/') and len(current_value) == 1:
|
|
170
|
+
self.show_command_menu()
|
|
171
|
+
# Clear the '/' from input
|
|
172
|
+
event.input.value = ""
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Show shortcuts menu when user types '?'
|
|
176
|
+
if current_value.endswith('?') and len(current_value) == 1:
|
|
177
|
+
self.show_shortcuts_menu()
|
|
178
|
+
# Clear the '?' from input
|
|
179
|
+
event.input.value = ""
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
183
|
+
"""Handle input submission"""
|
|
184
|
+
user_input = event.value.strip()
|
|
185
|
+
|
|
186
|
+
if not user_input:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Clear input
|
|
190
|
+
event.input.value = ""
|
|
191
|
+
|
|
192
|
+
if user_input.lower() in ['exit', 'quit', '/exit']:
|
|
193
|
+
self.exit()
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if user_input == '/clear':
|
|
197
|
+
self.action_clear()
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if user_input.startswith('/'):
|
|
201
|
+
self.handle_slash_command(user_input)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Regular message - send to AI
|
|
205
|
+
self.send_message(user_input)
|
|
206
|
+
|
|
207
|
+
def send_message(self, message: str) -> None:
|
|
208
|
+
"""Send message to AI and display response"""
|
|
209
|
+
messages = self.query_one("#messages", RichLog)
|
|
210
|
+
|
|
211
|
+
# Show user message
|
|
212
|
+
messages.write(f"[bold green]You[/bold green] › {message}")
|
|
213
|
+
|
|
214
|
+
# Add to conversation history
|
|
215
|
+
self.conversation_history.append({"role": "user", "content": message})
|
|
216
|
+
|
|
217
|
+
# Show thinking indicator
|
|
218
|
+
messages.write("[dim]TzamunCode is thinking...[/dim]")
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
# Get AI response (streaming)
|
|
222
|
+
response = ""
|
|
223
|
+
for chunk in self.client.chat_stream(self.conversation_history):
|
|
224
|
+
response += chunk
|
|
225
|
+
|
|
226
|
+
# Remove thinking indicator and show response
|
|
227
|
+
messages.clear()
|
|
228
|
+
|
|
229
|
+
# Re-show conversation
|
|
230
|
+
for msg in self.conversation_history:
|
|
231
|
+
if msg["role"] == "user":
|
|
232
|
+
messages.write(f"[bold green]You[/bold green] › {msg['content']}")
|
|
233
|
+
elif msg["role"] == "assistant":
|
|
234
|
+
messages.write(f"[bold blue]TzamunCode[/bold blue] › {msg['content']}")
|
|
235
|
+
|
|
236
|
+
# Show new response
|
|
237
|
+
messages.write(f"[bold blue]TzamunCode[/bold blue] › {response}")
|
|
238
|
+
messages.write("")
|
|
239
|
+
|
|
240
|
+
# Add to history
|
|
241
|
+
self.conversation_history.append({"role": "assistant", "content": response})
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
messages.write(f"[bold red]Error:[/bold red] {e}")
|
|
245
|
+
|
|
246
|
+
def show_command_menu(self) -> None:
|
|
247
|
+
"""Show command menu"""
|
|
248
|
+
messages = self.query_one("#messages", RichLog)
|
|
249
|
+
|
|
250
|
+
table = Table(title="Available Commands", border_style="cyan")
|
|
251
|
+
table.add_column("Command", style="cyan", no_wrap=True)
|
|
252
|
+
table.add_column("Description", style="white")
|
|
253
|
+
|
|
254
|
+
table.add_row("/help", "Show available commands")
|
|
255
|
+
table.add_row("/models", "List and switch models")
|
|
256
|
+
table.add_row("/settings", "Show settings")
|
|
257
|
+
table.add_row("/clear", "Clear conversation history")
|
|
258
|
+
table.add_row("/exit", "Exit chat")
|
|
259
|
+
|
|
260
|
+
messages.write(table)
|
|
261
|
+
messages.write("")
|
|
262
|
+
|
|
263
|
+
def show_shortcuts_menu(self) -> None:
|
|
264
|
+
"""Show shortcuts menu"""
|
|
265
|
+
messages = self.query_one("#messages", RichLog)
|
|
266
|
+
|
|
267
|
+
table = Table(title="Keyboard Shortcuts", border_style="blue")
|
|
268
|
+
table.add_column("Key", style="cyan", no_wrap=True)
|
|
269
|
+
table.add_column("Action", style="white")
|
|
270
|
+
|
|
271
|
+
table.add_row("Ctrl+C", "Exit chat")
|
|
272
|
+
table.add_row("Ctrl+L", "Clear screen")
|
|
273
|
+
table.add_row("/", "Show commands")
|
|
274
|
+
table.add_row("?", "Show this help")
|
|
275
|
+
|
|
276
|
+
messages.write(table)
|
|
277
|
+
messages.write("")
|
|
278
|
+
|
|
279
|
+
def handle_slash_command(self, command: str) -> None:
|
|
280
|
+
"""Handle slash commands"""
|
|
281
|
+
messages = self.query_one("#messages", RichLog)
|
|
282
|
+
|
|
283
|
+
if command == "/help":
|
|
284
|
+
self.show_command_menu()
|
|
285
|
+
elif command == "/models":
|
|
286
|
+
messages.write("[bold blue]Available Models:[/bold blue]")
|
|
287
|
+
try:
|
|
288
|
+
models = self.client.list_models()
|
|
289
|
+
for idx, model in enumerate(models, 1):
|
|
290
|
+
status = "✓ Active" if model == self.model else ""
|
|
291
|
+
messages.write(f" {idx}. {model} {status}")
|
|
292
|
+
except Exception as e:
|
|
293
|
+
messages.write(f"[bold red]Error:[/bold red] {e}")
|
|
294
|
+
messages.write("")
|
|
295
|
+
elif command == "/settings":
|
|
296
|
+
messages.write(f"[bold blue]Current Settings:[/bold blue]")
|
|
297
|
+
messages.write(f" Model: {self.model}")
|
|
298
|
+
messages.write(f" Streaming: Enabled")
|
|
299
|
+
messages.write("")
|
|
300
|
+
elif command == "/clear":
|
|
301
|
+
self.action_clear()
|
|
302
|
+
else:
|
|
303
|
+
messages.write(f"[bold red]Unknown command:[/bold red] {command}")
|
|
304
|
+
messages.write("[dim]Type '/' to see available commands[/dim]")
|
|
305
|
+
messages.write("")
|
|
306
|
+
|
|
307
|
+
def action_clear(self) -> None:
|
|
308
|
+
"""Clear conversation history"""
|
|
309
|
+
self.conversation_history = []
|
|
310
|
+
messages = self.query_one("#messages", RichLog)
|
|
311
|
+
messages.clear()
|
|
312
|
+
messages.write("[bold green]Conversation cleared[/bold green]")
|
|
313
|
+
messages.write("")
|
|
314
|
+
|
|
315
|
+
def action_quit(self) -> None:
|
|
316
|
+
"""Quit the app"""
|
|
317
|
+
self.exit()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def run_tui_chat(model: str = "qwen2.5:32b", system: Optional[str] = None):
|
|
321
|
+
"""Run the TUI chat interface"""
|
|
322
|
+
app = TUIChat(model=model)
|
|
323
|
+
app.run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration module"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Models module for AI backends"""
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ollama client for TzamunCode
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from typing import List, Dict, Generator, Optional
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
class OllamaClient:
|
|
10
|
+
"""Client for interacting with Ollama API"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
base_url: str = "http://localhost:11434",
|
|
15
|
+
model: str = "qwen2.5:32b",
|
|
16
|
+
timeout: int = 120
|
|
17
|
+
):
|
|
18
|
+
self.base_url = base_url.rstrip('/')
|
|
19
|
+
self.model = model
|
|
20
|
+
self.timeout = timeout
|
|
21
|
+
|
|
22
|
+
def generate(self, prompt: str, system: Optional[str] = None) -> str:
|
|
23
|
+
"""Generate a response from the model"""
|
|
24
|
+
url = f"{self.base_url}/api/generate"
|
|
25
|
+
|
|
26
|
+
payload = {
|
|
27
|
+
"model": self.model,
|
|
28
|
+
"prompt": prompt,
|
|
29
|
+
"stream": False,
|
|
30
|
+
"options": {
|
|
31
|
+
"temperature": 0.7,
|
|
32
|
+
"num_predict": 4096
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if system:
|
|
37
|
+
payload["system"] = system
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
response = requests.post(url, json=payload, timeout=self.timeout)
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
return response.json()["response"]
|
|
43
|
+
except requests.exceptions.RequestException as e:
|
|
44
|
+
raise Exception(f"Failed to generate response: {e}")
|
|
45
|
+
|
|
46
|
+
def chat(self, messages: List[Dict[str, str]]) -> str:
|
|
47
|
+
"""Chat with the model using conversation history"""
|
|
48
|
+
url = f"{self.base_url}/api/chat"
|
|
49
|
+
|
|
50
|
+
payload = {
|
|
51
|
+
"model": self.model,
|
|
52
|
+
"messages": messages,
|
|
53
|
+
"stream": False,
|
|
54
|
+
"options": {
|
|
55
|
+
"temperature": 0.7,
|
|
56
|
+
"num_predict": 4096
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
response = requests.post(url, json=payload, timeout=self.timeout)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
return response.json()["message"]["content"]
|
|
64
|
+
except requests.exceptions.RequestException as e:
|
|
65
|
+
raise Exception(f"Failed to chat: {e}")
|
|
66
|
+
|
|
67
|
+
def chat_stream(self, messages: List[Dict[str, str]]) -> Generator[str, None, None]:
|
|
68
|
+
"""Stream chat responses"""
|
|
69
|
+
url = f"{self.base_url}/api/chat"
|
|
70
|
+
|
|
71
|
+
payload = {
|
|
72
|
+
"model": self.model,
|
|
73
|
+
"messages": messages,
|
|
74
|
+
"stream": True,
|
|
75
|
+
"options": {
|
|
76
|
+
"temperature": 0.7,
|
|
77
|
+
"num_predict": 4096
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
response = requests.post(url, json=payload, timeout=self.timeout, stream=True)
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
|
|
85
|
+
for line in response.iter_lines():
|
|
86
|
+
if line:
|
|
87
|
+
data = json.loads(line)
|
|
88
|
+
if "message" in data:
|
|
89
|
+
content = data["message"].get("content", "")
|
|
90
|
+
if content:
|
|
91
|
+
yield content
|
|
92
|
+
|
|
93
|
+
if data.get("done", False):
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
except requests.exceptions.RequestException as e:
|
|
97
|
+
raise Exception(f"Failed to stream chat: {e}")
|
|
98
|
+
|
|
99
|
+
def list_models(self) -> List[str]:
|
|
100
|
+
"""List available models"""
|
|
101
|
+
url = f"{self.base_url}/api/tags"
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
response = requests.get(url, timeout=10)
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
models = response.json().get("models", [])
|
|
107
|
+
return [model["name"] for model in models]
|
|
108
|
+
except requests.exceptions.RequestException as e:
|
|
109
|
+
raise Exception(f"Failed to list models: {e}")
|
|
110
|
+
|
|
111
|
+
def pull_model(self, model_name: str) -> bool:
|
|
112
|
+
"""Pull a model from Ollama library"""
|
|
113
|
+
url = f"{self.base_url}/api/pull"
|
|
114
|
+
|
|
115
|
+
payload = {
|
|
116
|
+
"name": model_name
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
response = requests.post(url, json=payload, timeout=600)
|
|
121
|
+
response.raise_for_status()
|
|
122
|
+
return True
|
|
123
|
+
except requests.exceptions.RequestException as e:
|
|
124
|
+
raise Exception(f"Failed to pull model: {e}")
|