codemate-cli 1.0.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.
codemate/client.py ADDED
@@ -0,0 +1,311 @@
1
+ import httpx
2
+ import json
3
+ from typing import AsyncGenerator, Dict, List, Optional, Any, Tuple
4
+ from pathlib import Path
5
+
6
+ from codemate.config import Config
7
+
8
+
9
+ class CodeMateClient:
10
+ """Client for interacting with the CodeMate API"""
11
+
12
+ def __init__(self, config: Config):
13
+ self.config = config
14
+ self.base_url = config.get_endpoint()
15
+ self.api_key = config.get_api_key()
16
+ self.timeout = httpx.Timeout(120.0, connect=10.0)
17
+ self._kb_cache = None # Cache for knowledge bases
18
+
19
+ def _get_headers(self) -> Dict[str, str]:
20
+ """Get request headers with authorization"""
21
+ return {
22
+ "Content-Type": "application/json",
23
+ "Authorization": f"Bearer {self.api_key}",
24
+ }
25
+
26
+ def _prepare_messages(
27
+ self,
28
+ message: str,
29
+ conversation_history: Optional[List[Dict]] = None,
30
+ context: Optional[List[Dict]] = None
31
+ ) -> List[Dict]:
32
+ """Prepare messages array for API request"""
33
+ messages = conversation_history.copy() if conversation_history else []
34
+
35
+ # Add current message with context if provided
36
+ user_message = {
37
+ "role": "user",
38
+ "content": message
39
+ }
40
+
41
+ # If context is provided, add it to the message along with web_search flag
42
+ if context:
43
+ user_message["context"] = context
44
+ user_message["web_search"] = False
45
+ else:
46
+ # Even without context, add empty context and web_search false
47
+ user_message["context"] = []
48
+ user_message["web_search"] = False
49
+
50
+ messages.append(user_message)
51
+
52
+ return messages
53
+
54
+ async def chat_stream(
55
+ self,
56
+ message: str,
57
+ model: str = "chat_c0_cli",
58
+ conversation_history: Optional[List[Dict]] = None,
59
+ conversation_id: Optional[str] = None,
60
+ call_for: str = "chat_c0_cli",
61
+ tools: Optional[List[Dict]] = None,
62
+ context: Optional[List[Dict]] = None,
63
+ ) -> AsyncGenerator[Dict[str, Any], None]:
64
+ """
65
+ Stream chat completions from the API
66
+
67
+ Yields dictionaries with:
68
+ - type: "message" | "reasoning" | "tool_call" | "tool_result" | "metadata"
69
+ - content: the actual content
70
+ - Additional fields depending on type
71
+ """
72
+ messages = self._prepare_messages(message, conversation_history, context)
73
+
74
+ payload = {
75
+ "messages": messages,
76
+ "call_for": "chat_c0_cli",
77
+ "mode": "NORMAL",
78
+ "conversation_id": conversation_id,
79
+ }
80
+
81
+ client = httpx.AsyncClient(timeout=self.timeout)
82
+ stream_context = None
83
+ reasoning_content = ""
84
+ message_content = ""
85
+
86
+ try:
87
+ stream_context = client.stream(
88
+ "POST",
89
+ f"{self.base_url}/chat/stream",
90
+ headers=self._get_headers(),
91
+ json=payload,
92
+ )
93
+ response = await stream_context.__aenter__()
94
+ response.raise_for_status()
95
+
96
+ buffer = ""
97
+ should_stop = False
98
+
99
+ async for chunk in response.aiter_text():
100
+ # with open("extra.json", "a", encoding="utf-8") as f:
101
+ # f.write(chunk)
102
+ # f.write("\n")
103
+
104
+ if should_stop:
105
+ break
106
+
107
+ buffer += chunk
108
+
109
+ while "<__!!__END__!!__>" in buffer:
110
+ msg_str, buffer = buffer.split("<__!!__END__!!__>", 1)
111
+ msg_str = msg_str.strip()
112
+
113
+ if not msg_str:
114
+ continue
115
+
116
+ try:
117
+ chunk_data = json.loads(msg_str)
118
+ msg_type = chunk_data.get("type")
119
+
120
+ # ADD ERROR HANDLING:
121
+ if msg_type == "error":
122
+ yield {
123
+ "type": "error",
124
+ "message": chunk_data.get("message", "Unknown error")
125
+ }
126
+ should_stop = True
127
+ break
128
+
129
+ elif msg_type == "end":
130
+ should_stop = True
131
+ break
132
+ elif msg_type == "message":
133
+ content = chunk_data.get("message")
134
+ reasoning = chunk_data.get("reasoning_content")
135
+ if content:
136
+ message_content += content
137
+ yield {"type": "message", "content": content}
138
+ elif reasoning:
139
+ reasoning_content += reasoning
140
+ yield {"type": "reasoning", "content": reasoning}
141
+ elif msg_type == "tool_calls":
142
+ # Yield tool calls information
143
+ yield {
144
+ "type": "tool_calls",
145
+ "tool_calls": chunk_data.get("tool_calls", [])
146
+ }
147
+ elif msg_type == "tool_call_start":
148
+ # Yield tool call start
149
+ yield {
150
+ "type": "tool_call_start",
151
+ "tool_call": chunk_data.get("tool_call", {})
152
+ }
153
+ elif msg_type == "tool_call_complete":
154
+ # Yield tool call completion with results
155
+ yield {
156
+ "type": "tool_call_complete",
157
+ "tool_call": chunk_data.get("tool_call", {})
158
+ }
159
+ elif msg_type == "usage":
160
+ # Yield usage information
161
+ yield {
162
+ "type": "usage",
163
+ "usage": chunk_data.get("usage", {})
164
+ }
165
+
166
+ except json.JSONDecodeError as e:
167
+ print(f"JSON decode error: {e}, chunk: {msg_str[:100]}")
168
+ continue
169
+
170
+ # Process remaining buffer
171
+ if buffer.strip() and not should_stop:
172
+ try:
173
+ chunk_data = json.loads(buffer.strip())
174
+ msg_type = chunk_data.get("type")
175
+ if msg_type == "message":
176
+ content = chunk_data.get("message")
177
+ reasoning = chunk_data.get("reasoning_content")
178
+ if content:
179
+ message_content += content
180
+ elif reasoning:
181
+ reasoning_content += reasoning
182
+ except json.JSONDecodeError:
183
+ pass
184
+
185
+ # Write to files after stream completes
186
+ # with open("reasoning_content.txt", "w", encoding="utf-8") as f:
187
+ # f.write(reasoning_content)
188
+ # with open("message_content.txt", "w", encoding="utf-8") as f:
189
+ # f.write(message_content)
190
+
191
+ finally:
192
+ # ✅ Ensure cleanup happens
193
+ if stream_context:
194
+ try:
195
+ await stream_context.__aexit__(None, None, None)
196
+ except Exception:
197
+ pass
198
+
199
+ try:
200
+ await client.aclose()
201
+ except Exception:
202
+ pass
203
+
204
+
205
+ def chat(
206
+ self,
207
+ message: str,
208
+ model: str = "chat_c0_cli",
209
+ conversation_history: Optional[List[Dict]] = None,
210
+ conversation_id: Optional[str] = None,
211
+ call_for: str = "chat_c0_cli",
212
+ tools: Optional[List[Dict]] = None,
213
+ context: Optional[List[Dict]] = None,
214
+ ) -> Dict[str, Any]:
215
+ """
216
+ Send a chat completion request (non-streaming)
217
+
218
+ Returns the complete response
219
+ """
220
+ messages = self._prepare_messages(message, conversation_history, context)
221
+
222
+ payload = {
223
+ "messages": messages,
224
+ "call_for": "chat_c0_cli",
225
+ "mode": "NORMAL",
226
+ "conversation_id": conversation_id,
227
+ }
228
+
229
+ with httpx.Client(timeout=self.timeout) as client:
230
+ response = client.post(
231
+ f"{self.base_url}/chat/stream",
232
+ headers=self._get_headers(),
233
+ json=payload,
234
+ )
235
+ response.raise_for_status()
236
+ return response.json()
237
+
238
+ async def get_models(self) -> List[Dict[str, Any]]:
239
+ """Get list of available models"""
240
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
241
+ response = await client.get(
242
+ f"{self.base_url}/v1/models",
243
+ headers=self._get_headers(),
244
+ )
245
+ response.raise_for_status()
246
+ return response.json()
247
+
248
+ async def list_kbs(self) -> List[Dict[str, Any]]:
249
+ """Get list of knowledge bases"""
250
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
251
+ response = await client.get(
252
+ f"{self.base_url}/list_kbs?include_cloud=false",
253
+ headers=self._get_headers(),
254
+ )
255
+ response.raise_for_status()
256
+ data = response.json()
257
+ # Cache the knowledge bases
258
+ self._kb_cache = data
259
+ return data
260
+
261
+ def get_cached_kbs(self) -> Optional[List[Dict[str, Any]]]:
262
+ """Get cached knowledge bases"""
263
+ return self._kb_cache
264
+
265
+
266
+ # Synchronous wrapper for CLI usage
267
+ class SyncCodeMateClient:
268
+ """Synchronous wrapper for the async client"""
269
+
270
+ def __init__(self, config: Config):
271
+ self.async_client = CodeMateClient(config)
272
+
273
+ def chat_stream(self, *args, **kwargs):
274
+ """Stream chat - returns async generator"""
275
+ return self.async_client.chat_stream(*args, **kwargs)
276
+
277
+ def chat(self, *args, **kwargs):
278
+ """Non-streaming chat"""
279
+ return self.async_client.chat(*args, **kwargs)
280
+
281
+ def list_kbs(self):
282
+ """List knowledge bases"""
283
+ import asyncio
284
+
285
+ try:
286
+ # Check if there's already a running event loop
287
+ loop = asyncio.get_running_loop()
288
+ # If we're in an async context, we need to create a new task and wait for it
289
+ import concurrent.futures
290
+ import threading
291
+
292
+ def run_in_thread():
293
+ """Run the async method in a separate thread with its own event loop"""
294
+ new_loop = asyncio.new_event_loop()
295
+ asyncio.set_event_loop(new_loop)
296
+ try:
297
+ return new_loop.run_until_complete(self.async_client.list_kbs())
298
+ finally:
299
+ new_loop.close()
300
+
301
+ with concurrent.futures.ThreadPoolExecutor() as executor:
302
+ future = executor.submit(run_in_thread)
303
+ return future.result()
304
+
305
+ except RuntimeError:
306
+ # No running event loop, safe to use asyncio.run()
307
+ return asyncio.run(self.async_client.list_kbs())
308
+
309
+ def get_cached_kbs(self):
310
+ """Get cached knowledge bases"""
311
+ return self.async_client.get_cached_kbs()
@@ -0,0 +1,6 @@
1
+ """
2
+ Command handlers for CodeMate CLI
3
+ """
4
+
5
+ # This module will contain individual command implementations
6
+ # Commands are defined in cli.py using Click decorators
File without changes
@@ -0,0 +1,103 @@
1
+ """
2
+ Configuration command implementation
3
+ """
4
+
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from rich.panel import Panel
8
+
9
+ from codemate.config import Config
10
+
11
+
12
+ class ConfigCommand:
13
+ """Handler for configuration commands"""
14
+
15
+ def __init__(self, config: Config, console: Console):
16
+ self.config = config
17
+ self.console = console
18
+
19
+ def set_api_key(self, api_key: str):
20
+ """Set API key"""
21
+ if not api_key or not api_key.strip():
22
+ self.console.print("[red]Error: API key cannot be empty[/red]")
23
+ return False
24
+
25
+ self.config.set_api_key(api_key)
26
+ self.console.print("[green]✓ API key saved successfully![/green]")
27
+ return True
28
+
29
+ def get_api_key(self):
30
+ """Display masked API key"""
31
+ key = self.config.get_api_key()
32
+ if key:
33
+ masked = key[:8] + "..." + key[-4:] if len(key) > 12 else "***"
34
+ self.console.print(f"API Key: [cyan]{masked}[/cyan]")
35
+ else:
36
+ self.console.print("[yellow]No API key configured.[/yellow]")
37
+ self.console.print(
38
+ "Set one with: [cyan]codemate config set-key YOUR_API_KEY[/cyan]"
39
+ )
40
+
41
+ def set_endpoint(self, endpoint: str):
42
+ """Set API endpoint"""
43
+ if not endpoint or not endpoint.strip():
44
+ self.console.print("[red]Error: Endpoint cannot be empty[/red]")
45
+ return False
46
+
47
+ # Basic URL validation
48
+ if not endpoint.startswith(("http://", "https://")):
49
+ self.console.print(
50
+ "[yellow]Warning: Endpoint should start with http:// or https://[/yellow]"
51
+ )
52
+
53
+ self.config.set_endpoint(endpoint)
54
+ self.console.print(f"[green]✓ Endpoint set to: {endpoint}[/green]")
55
+ return True
56
+
57
+ def get_endpoint(self):
58
+ """Display current endpoint"""
59
+ endpoint = self.config.get_endpoint()
60
+ self.console.print(f"Endpoint: [cyan]{endpoint}[/cyan]")
61
+
62
+ def set_model(self, model: str):
63
+ """Set default model"""
64
+ self.config.set_default_model(model)
65
+ self.console.print(f"[green]✓ Default model set to: {model}[/green]")
66
+
67
+ def show_all(self):
68
+ """Display all configuration settings"""
69
+ settings = self.config.get_all()
70
+
71
+ table = Table(
72
+ title="Current Configuration",
73
+ show_header=True,
74
+ header_style="bold magenta"
75
+ )
76
+ table.add_column("Setting", style="cyan", width=20)
77
+ table.add_column("Value", style="white")
78
+
79
+ for key, value in settings.items():
80
+ # Mask API key
81
+ if key == "api_key" and value:
82
+ display_value = value[:8] + "..." + value[-4:] if len(value) > 12 else "***"
83
+ else:
84
+ display_value = str(value)
85
+
86
+ table.add_row(key, display_value)
87
+
88
+ self.console.print(table)
89
+
90
+ def reset(self):
91
+ """Reset configuration to defaults"""
92
+ self.console.print("[yellow]Resetting configuration to defaults...[/yellow]")
93
+ self.config.reset()
94
+ self.console.print("[green]✓ Configuration reset successfully![/green]")
95
+
96
+ def show_location(self):
97
+ """Show configuration file location"""
98
+ self.console.print(Panel(
99
+ f"Configuration file location:\n[cyan]{self.config.config_file}[/cyan]\n\n"
100
+ f"History directory:\n[cyan]{self.config.config_dir / 'history'}[/cyan]",
101
+ title="[bold]Config Location[/bold]",
102
+ border_style="cyan"
103
+ ))