chat-console 0.1.1__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.
app/ui/styles.py ADDED
@@ -0,0 +1,275 @@
1
+ from rich.style import Style
2
+ from rich.theme import Theme
3
+ from textual.widget import Widget
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Container, Horizontal, Vertical
6
+ from textual.css.query import NoMatches
7
+
8
+ # Define color palette
9
+ COLORS = {
10
+ "dark": {
11
+ "background": "#0C0C0C",
12
+ "foreground": "#33FF33",
13
+ "user_msg": "#00FFFF",
14
+ "assistant_msg": "#33FF33",
15
+ "system_msg": "#FF8C00",
16
+ "highlight": "#FFD700",
17
+ "selection": "#1A1A1A",
18
+ "border": "#33FF33",
19
+ "error": "#FF0000",
20
+ "success": "#33FF33",
21
+ },
22
+ "light": {
23
+ "background": "#F0F0F0",
24
+ "foreground": "#000000",
25
+ "user_msg": "#0000FF",
26
+ "assistant_msg": "#008000",
27
+ "system_msg": "#800080",
28
+ "highlight": "#0078D7",
29
+ "selection": "#ADD6FF",
30
+ "border": "#D0D0D0",
31
+ "error": "#D32F2F",
32
+ "success": "#388E3C",
33
+ }
34
+ }
35
+
36
+ def get_theme(theme_name="dark"):
37
+ """Get Rich theme based on theme name"""
38
+ colors = COLORS.get(theme_name, COLORS["dark"])
39
+
40
+ return Theme({
41
+ "user": Style(color=colors["user_msg"], bold=True),
42
+ "assistant": Style(color=colors["assistant_msg"]),
43
+ "system": Style(color=colors["system_msg"], italic=True),
44
+ "highlight": Style(color=colors["highlight"], bold=True),
45
+ "selection": Style(bgcolor=colors["selection"]),
46
+ "border": Style(color=colors["border"]),
47
+ "error": Style(color=colors["error"], bold=True),
48
+ "success": Style(color=colors["success"]),
49
+ "prompt": Style(color=colors["highlight"]),
50
+ "heading": Style(color=colors["highlight"], bold=True),
51
+ "dim": Style(color=colors["border"]),
52
+ "code": Style(bgcolor="#2D2D2D", color="#D4D4D4"),
53
+ "code.syntax": Style(color="#569CD6"),
54
+ "link": Style(color=colors["highlight"], underline=True),
55
+ })
56
+
57
+ # Textual CSS for the application
58
+ CSS = """
59
+ /* Base styles */
60
+ Screen {
61
+ background: $surface;
62
+ color: $text;
63
+ }
64
+
65
+ /* Chat message styles */
66
+ .message {
67
+ width: 100%;
68
+ padding: 0 1;
69
+ margin: 0;
70
+ }
71
+
72
+ .message-content {
73
+ width: 100%;
74
+ text-align: left;
75
+ padding: 0;
76
+ }
77
+
78
+ /* Code blocks */
79
+ .code-block {
80
+ background: $surface-darken-3;
81
+ color: $text-muted;
82
+ border: solid $primary-darken-3;
83
+ margin: 1 2;
84
+ padding: 1;
85
+ overflow: auto;
86
+ }
87
+
88
+ /* Input area */
89
+ #input-container {
90
+ height: auto;
91
+ background: $surface;
92
+ border-top: solid $primary-darken-2;
93
+ padding: 0;
94
+ }
95
+
96
+ #message-input {
97
+ background: $surface-darken-1;
98
+ color: $text;
99
+ border: solid $primary-darken-2;
100
+ min-height: 2;
101
+ padding: 0 1;
102
+ }
103
+
104
+ #message-input:focus {
105
+ border: tall $primary;
106
+ }
107
+
108
+ /* Action buttons */
109
+ .action-button {
110
+ background: $primary;
111
+ color: $text;
112
+ border: none;
113
+ min-width: 10;
114
+ margin-left: 1;
115
+ }
116
+
117
+ .action-button:hover {
118
+ background: $primary-lighten-1;
119
+ }
120
+
121
+ /* Sidebar */
122
+ #sidebar {
123
+ width: 25%;
124
+ min-width: 18;
125
+ background: $surface-darken-1;
126
+ border-right: solid $primary-darken-2 1;
127
+ }
128
+
129
+ /* Chat list */
130
+ .chat-item {
131
+ padding: 0 1;
132
+ height: 2;
133
+ border-bottom: solid $primary-darken-3 1;
134
+ }
135
+
136
+ .chat-item:hover {
137
+ background: $primary-darken-2;
138
+ }
139
+
140
+ .chat-item.selected {
141
+ background: $primary-darken-1;
142
+ border-left: wide $primary;
143
+ }
144
+
145
+ .chat-title {
146
+ width: 100%;
147
+ content-align: center middle;
148
+ text-align: left;
149
+ }
150
+
151
+ .chat-model {
152
+ color: $text-muted;
153
+ text-align: right;
154
+ }
155
+
156
+ .chat-date {
157
+ color: $text-muted;
158
+ text-align: right;
159
+ }
160
+
161
+ /* Search input */
162
+ #search-input {
163
+ width: 100%;
164
+ border: solid $primary-darken-2 1;
165
+ margin: 0 1;
166
+ height: 2;
167
+ }
168
+
169
+ #search-input:focus {
170
+ border: solid $primary;
171
+ }
172
+
173
+ /* Model selector */
174
+ #model-selector {
175
+ width: 100%;
176
+ height: 2;
177
+ margin: 0 1;
178
+ background: $surface-darken-1;
179
+ border: solid $primary-darken-2 1;
180
+ }
181
+
182
+ /* Style selector */
183
+ #style-selector {
184
+ width: 100%;
185
+ height: 2;
186
+ margin: 0 1;
187
+ background: $surface-darken-1;
188
+ border: solid $primary-darken-2 1;
189
+ }
190
+
191
+ /* Header */
192
+ #app-header {
193
+ width: 100%;
194
+ height: 2;
195
+ background: $surface-darken-2;
196
+ color: $text;
197
+ content-align: center middle;
198
+ text-align: center;
199
+ border-bottom: solid $primary-darken-2 1;
200
+ }
201
+
202
+ /* Loading indicator */
203
+ #loading-indicator {
204
+ background: $surface-darken-1;
205
+ color: $text;
206
+ padding: 0 1;
207
+ height: auto;
208
+ width: 100%;
209
+ border-top: solid $primary-darken-2 1;
210
+ display: none;
211
+ }
212
+
213
+ /* Settings modal */
214
+ .modal {
215
+ background: $surface;
216
+ border: solid $primary;
217
+ padding: 1;
218
+ height: auto;
219
+ min-width: 40;
220
+ max-width: 60;
221
+ }
222
+
223
+ .modal-title {
224
+ background: $primary;
225
+ color: $text;
226
+ width: 100%;
227
+ height: 3;
228
+ content-align: center middle;
229
+ text-align: center;
230
+ }
231
+
232
+ .form-label {
233
+ width: 100%;
234
+ padding: 1 0;
235
+ }
236
+
237
+ .form-input {
238
+ width: 100%;
239
+ background: $surface-darken-1;
240
+ border: solid $primary-darken-2;
241
+ height: 3;
242
+ margin-bottom: 1;
243
+ }
244
+
245
+ .form-input:focus {
246
+ border: solid $primary;
247
+ }
248
+
249
+ .button-container {
250
+ width: 100%;
251
+ height: 3;
252
+ align: right middle;
253
+ }
254
+
255
+ .button {
256
+ background: $primary-darken-1;
257
+ color: $text;
258
+ min-width: 6;
259
+ margin-left: 1;
260
+ border: solid $primary 1;
261
+ }
262
+
263
+ .button.cancel {
264
+ background: $error;
265
+ }
266
+
267
+ /* Tags */
268
+ .tag {
269
+ background: $primary-darken-1;
270
+ color: $text;
271
+ padding: 0 1;
272
+ margin: 0 1 0 0;
273
+ border: solid $border;
274
+ }
275
+ """
app/utils.py ADDED
@@ -0,0 +1,202 @@
1
+ from datetime import datetime
2
+ import re
3
+ import asyncio
4
+ import time
5
+ from typing import List, Dict, Any, Optional, Generator, Awaitable, Callable
6
+ import textwrap
7
+ import threading
8
+ from rich.text import Text
9
+ from rich.markdown import Markdown
10
+ from rich.syntax import Syntax
11
+ from rich.panel import Panel
12
+ from rich.console import Console
13
+
14
+ from .models import Message, Conversation
15
+ from .database import ChatDatabase
16
+ from .api.base import BaseModelClient
17
+
18
+ def generate_conversation_title(messages: List[Message], model: str) -> str:
19
+ """Generate a title for a conversation based on its content"""
20
+ # Find the first user message
21
+ first_user_message = None
22
+ for msg in messages:
23
+ if msg.role == "user":
24
+ first_user_message = msg
25
+ break
26
+
27
+ if first_user_message is None:
28
+ return f"New conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
29
+
30
+ # Use first line of the first user message (up to 40 chars)
31
+ content = first_user_message.content.strip()
32
+
33
+ # Get first line
34
+ first_line = content.split('\n')[0]
35
+
36
+ # Truncate if needed
37
+ if len(first_line) > 40:
38
+ title = first_line[:37] + "..."
39
+ else:
40
+ title = first_line
41
+
42
+ return title
43
+
44
+ def format_code_blocks(text: str) -> str:
45
+ """Ensure code blocks have proper formatting"""
46
+ # Make sure code blocks are properly formatted with triple backticks
47
+ pattern = r"```(\w*)\n(.*?)\n```"
48
+
49
+ def code_replace(match):
50
+ lang = match.group(1)
51
+ code = match.group(2)
52
+ # Ensure code has proper indentation
53
+ code_lines = code.split('\n')
54
+ code = '\n'.join([line.rstrip() for line in code_lines])
55
+ return f"```{lang}\n{code}\n```"
56
+
57
+ return re.sub(pattern, code_replace, text, flags=re.DOTALL)
58
+
59
+ def extract_code_blocks(text: str) -> List[Dict[str, str]]:
60
+ """Extract code blocks from text content"""
61
+ blocks = []
62
+ pattern = r"```(\w*)\n(.*?)\n```"
63
+ matches = re.finditer(pattern, text, re.DOTALL)
64
+
65
+ for match in matches:
66
+ lang = match.group(1) or "text"
67
+ code = match.group(2).strip()
68
+ blocks.append({
69
+ "language": lang,
70
+ "code": code,
71
+ "start": match.start(),
72
+ "end": match.end()
73
+ })
74
+
75
+ return blocks
76
+
77
+ def format_text(text: str, highlight_code: bool = True) -> Text:
78
+ """Format text with optional code highlighting"""
79
+ result = Text()
80
+
81
+ if not highlight_code:
82
+ return Text(text)
83
+
84
+ # Split by code blocks
85
+ parts = re.split(r'(```\w*\n.*?\n```)', text, flags=re.DOTALL)
86
+
87
+ for part in parts:
88
+ if part.startswith('```'):
89
+ # Handle code block
90
+ match = re.match(r'```(\w*)\n(.*?)\n```', part, re.DOTALL)
91
+ if match:
92
+ lang = match.group(1) or "text"
93
+ code = match.group(2).strip()
94
+ syntax = Syntax(
95
+ code,
96
+ lang,
97
+ theme="monokai",
98
+ line_numbers=True,
99
+ word_wrap=True,
100
+ indent_guides=True
101
+ )
102
+ result.append("\n")
103
+ result.append(syntax)
104
+ result.append("\n")
105
+ else:
106
+ # Handle regular text
107
+ if part.strip():
108
+ result.append(Text(part.strip()))
109
+ result.append("\n")
110
+
111
+ return result
112
+
113
+ def create_new_conversation(db: ChatDatabase, model: str, style: str = "default") -> Conversation:
114
+ """Create a new conversation in the database"""
115
+ title = f"New conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
116
+ conversation_id = db.create_conversation(title, model, style)
117
+
118
+ # Get full conversation object
119
+ conversation_data = db.get_conversation(conversation_id)
120
+ return Conversation.from_dict(conversation_data)
121
+
122
+ def update_conversation_title(db: ChatDatabase, conversation: Conversation) -> None:
123
+ """Update the title of a conversation based on its content"""
124
+ if not conversation.messages:
125
+ return
126
+
127
+ title = generate_conversation_title(conversation.messages, conversation.model)
128
+ db.update_conversation(conversation.id, title=title)
129
+ conversation.title = title
130
+
131
+ def add_message_to_conversation(
132
+ db: ChatDatabase,
133
+ conversation: Conversation,
134
+ role: str,
135
+ content: str
136
+ ) -> Message:
137
+ """Add a message to a conversation in the database"""
138
+ message_id = db.add_message(conversation.id, role, content)
139
+
140
+ # Create message object
141
+ message = Message(
142
+ id=message_id,
143
+ conversation_id=conversation.id,
144
+ role=role,
145
+ content=content,
146
+ timestamp=datetime.now().isoformat()
147
+ )
148
+
149
+ # Add to conversation
150
+ conversation.messages.append(message)
151
+
152
+ # Update conversation title if it's the default
153
+ if conversation.title.startswith("New conversation"):
154
+ update_conversation_title(db, conversation)
155
+
156
+ return message
157
+
158
+ def run_in_thread(func: Callable, *args, **kwargs) -> threading.Thread:
159
+ """Run a function in a separate thread"""
160
+ thread = threading.Thread(target=func, args=args, kwargs=kwargs)
161
+ thread.daemon = True
162
+ thread.start()
163
+ return thread
164
+
165
+ async def generate_streaming_response(
166
+ messages: List[Dict[str, str]],
167
+ model: str,
168
+ style: str,
169
+ client: BaseModelClient,
170
+ callback: Callable[[str], Awaitable[None]]
171
+ ) -> str:
172
+ """Generate a streaming response and call the callback for each chunk"""
173
+ full_response = ""
174
+
175
+ try:
176
+ # Get the async generator from the client
177
+ stream = client.generate_stream(messages, model, style)
178
+ # Iterate over the generator properly
179
+ async for chunk in stream:
180
+ if chunk: # Only process non-empty chunks
181
+ full_response += chunk
182
+ # Update UI and ensure event loop processes it
183
+ await callback(chunk)
184
+ # Small delay to prevent overwhelming the event loop
185
+ await asyncio.sleep(0.01)
186
+ except Exception as e:
187
+ error_msg = f"\n\nError generating response: {str(e)}"
188
+ full_response += error_msg
189
+ await callback(error_msg)
190
+
191
+ return full_response
192
+
193
+ def get_elapsed_time(start_time: float) -> str:
194
+ """Get the elapsed time as a formatted string"""
195
+ elapsed = time.time() - start_time
196
+
197
+ if elapsed < 60:
198
+ return f"{elapsed:.1f}s"
199
+ else:
200
+ minutes = int(elapsed // 60)
201
+ seconds = elapsed % 60
202
+ return f"{minutes}m {seconds:.1f}s"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.2
2
+ Name: chat-console
3
+ Version: 0.1.1
4
+ Summary: A command-line interface for chatting with LLMs, storing chats and (future) rag interactions
5
+ Home-page: https://github.com/wazacraftrfid/chat-console
6
+ Author: Johnathan Greenaway
7
+ Author-email: john@fimbriata.dev
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: textual>=0.11.1
15
+ Requires-Dist: typer>=0.7.0
16
+ Requires-Dist: requests>=2.28.1
17
+ Requires-Dist: anthropic>=0.5.0
18
+ Requires-Dist: openai>=0.27.0
19
+ Requires-Dist: python-dotenv>=0.21.0
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ # Chat CLI
31
+
32
+ A comprehensive command-line interface for chatting with various AI language models. This application allows you to interact with different LLM providers through an intuitive terminal-based interface.
33
+
34
+ ## Features
35
+
36
+ - Interactive terminal UI with Textual library
37
+ - Support for multiple AI models:
38
+ - OpenAI models (GPT-3.5, GPT-4)
39
+ - Anthropic models (Claude 3 Opus, Sonnet, Haiku)
40
+ - Conversation history with search functionality
41
+ - Customizable response styles (concise, detailed, technical, friendly)
42
+ - Code syntax highlighting
43
+ - Markdown rendering
44
+
45
+ ## Installation
46
+
47
+ 1. Clone this repository:
48
+ ```
49
+ git clone https://github.com/yourusername/chat-cli.git
50
+ cd chat-cli
51
+ ```
52
+
53
+ 2. Install the required dependencies:
54
+ ```
55
+ pip install -r requirements.txt
56
+ ```
57
+
58
+ 3. Set up your API keys:
59
+
60
+ Create a `.env` file in the project root directory with your API keys:
61
+ ```
62
+ OPENAI_API_KEY=your_openai_api_key_here
63
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ Run the application:
69
+ ```
70
+ chat-cli
71
+ ```
72
+
73
+ ### Keyboard Shortcuts
74
+
75
+ - `q` - Quit the application
76
+ - `n` - Start a new conversation
77
+ - `s` - Toggle sidebar
78
+ - `f` - Focus search box
79
+ - `Escape` - Cancel current generation
80
+ - `Ctrl+C` - Quit the application
81
+
82
+ ### Configuration
83
+
84
+ The application creates a configuration file at `~/.chatcli/config.json` on first run. You can edit this file to:
85
+
86
+ - Change the default model
87
+ - Modify available models
88
+ - Add or edit response styles
89
+ - Change the theme
90
+ - Adjust other settings
91
+
92
+ ## Data Storage
93
+
94
+ Conversation history is stored in a SQLite database at `~/.chatcli/chat_history.db`.
95
+
96
+ ## Development
97
+
98
+ The application is structured as follows:
99
+
100
+ - `main.py` - Main application entry point
101
+ - `app/` - Application modules
102
+ - `api/` - LLM provider API client implementations
103
+ - `ui/` - User interface components
104
+ - `config.py` - Configuration management
105
+ - `database.py` - Database operations
106
+ - `models.py` - Data models
107
+ - `utils.py` - Utility functions
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,23 @@
1
+ app/__init__.py,sha256=u5X4kPcpqZ12ZLnhwwOCScNvftaknDTrb0DMXqR_iLc,130
2
+ app/config.py,sha256=PLEic_jwfWvWJxDfQMbKSbJ4ULrcmDhVe0apqegMO_g,3571
3
+ app/database.py,sha256=nt8CVuDpy6zw8mOYqDcfUmNw611t7Ln7pz22M0b6-MI,9967
4
+ app/main.py,sha256=B_E8ovyGX1xLXDJF_nkjQcbX_AP5hLDih3jqJXdHMMY,19848
5
+ app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
6
+ app/utils.py,sha256=oUpQpqrxvvQn0S0lMCSwDC1Rx0PHpoAIRDySohYV5Oo,6586
7
+ app/api/__init__.py,sha256=A8UL84ldYlv8l7O-yKzraVFcfww86SgWfpl4p7R03-w,62
8
+ app/api/anthropic.py,sha256=leWSnCfqKnxHB5k3l_oVty4km3q18dodJkPAxwvhEt0,4211
9
+ app/api/base.py,sha256=-Lx6nfgvEPjrAnQXuCgG-zr8soD1AibTtP15gVD3O48,3138
10
+ app/api/ollama.py,sha256=naD5-WVCthZ-0s4iBo_bYV1hRMcuczly-lghmB2_loQ,5033
11
+ app/api/openai.py,sha256=70NITI4upGld_xpaCZLoMd0ObSeVdhtiyUfY9hYHlhE,3420
12
+ app/ui/__init__.py,sha256=RndfbQ1Tv47qdSiuQzvWP96lPS547SDaGE-BgOtiP_w,55
13
+ app/ui/chat_interface.py,sha256=5gSOa7zT9bWujkPYctB8gVm4yypnkmKHcY1VtaKcEQs,11126
14
+ app/ui/chat_list.py,sha256=WQTYVNSSXlx_gQal3YqILZZKL9UiTjmNMIDX2I9pAMM,11205
15
+ app/ui/model_selector.py,sha256=Rv0i2VjLL2-cp4Pn_uMnAnAIV7Zk9gBX1XoWKBzkxHg,10367
16
+ app/ui/search.py,sha256=b-m14kG3ovqW1-i0qDQ8KnAqFJbi5b1FLM9dOnbTyIs,9763
17
+ app/ui/styles.py,sha256=eVDBTpBGnQ-mg5SeLi6i74ZjhCpItxAwWh1IelD09GY,5445
18
+ chat_console-0.1.1.dist-info/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
19
+ chat_console-0.1.1.dist-info/METADATA,sha256=Nhshut-tQIN9whzOfxVBNYVAjx6wG90KoyzLgpNeFlg,2899
20
+ chat_console-0.1.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
21
+ chat_console-0.1.1.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
22
+ chat_console-0.1.1.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
23
+ chat_console-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (76.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ c-c = app.main:main
3
+ chat-console = app.main:main
@@ -0,0 +1 @@
1
+ app