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.
@@ -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}")