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/__init__.py +17 -0
- codemate/__main__.py +10 -0
- codemate/cli.py +815 -0
- codemate/client.py +311 -0
- codemate/commands/__init__.py +6 -0
- codemate/commands/chat.py +0 -0
- codemate/commands/config.py +103 -0
- codemate/commands/help.py +298 -0
- codemate/commands/kb_commands.py +749 -0
- codemate/config.py +233 -0
- codemate/ui/__init__.py +10 -0
- codemate/ui/markdown.py +212 -0
- codemate/ui/renderer.py +159 -0
- codemate/ui/streaming.py +436 -0
- codemate/utils/__init__.py +21 -0
- codemate/utils/auth.py +164 -0
- codemate/utils/error_handler.py +277 -0
- codemate/utils/errors.py +156 -0
- codemate/utils/kb_parser.py +111 -0
- codemate_cli-1.0.0.dist-info/METADATA +452 -0
- codemate_cli-1.0.0.dist-info/RECORD +25 -0
- codemate_cli-1.0.0.dist-info/WHEEL +5 -0
- codemate_cli-1.0.0.dist-info/entry_points.txt +3 -0
- codemate_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- codemate_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# codemate/utils/error_handler.py
|
|
2
|
+
"""Enhanced error handling for CodeMate CLI"""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import webbrowser
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich import box
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ErrorHandler:
|
|
14
|
+
"""Centralized error handling for CodeMate CLI"""
|
|
15
|
+
|
|
16
|
+
# Upgrade URL for plan limits
|
|
17
|
+
UPGRADE_URL = "https://app.codemate.ai/settings"
|
|
18
|
+
|
|
19
|
+
def __init__(self, console: Console):
|
|
20
|
+
self.console = console
|
|
21
|
+
|
|
22
|
+
def parse_error_message(self, error_msg: str) -> Tuple[str, str, Optional[str]]:
|
|
23
|
+
"""
|
|
24
|
+
Parse error message and categorize it
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Tuple of (error_type, display_message, upgrade_url)
|
|
28
|
+
- error_type: "connection", "rate_limit", "server", "generic"
|
|
29
|
+
- display_message: User-friendly message
|
|
30
|
+
- upgrade_url: URL for upgrade (if rate limit error)
|
|
31
|
+
"""
|
|
32
|
+
error_lower = error_msg.lower()
|
|
33
|
+
|
|
34
|
+
# Connection errors
|
|
35
|
+
if "connection" in error_lower or "failed to connect" in error_lower:
|
|
36
|
+
return (
|
|
37
|
+
"connection",
|
|
38
|
+
"Unable to connect to CodeMate server. Please check:\n"
|
|
39
|
+
" • Is the server running?\n"
|
|
40
|
+
" • Check your internet connection\n"
|
|
41
|
+
" • Verify server endpoint in configuration",
|
|
42
|
+
None
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# All connection attempts failed
|
|
46
|
+
if "all connection attempts failed" in error_lower:
|
|
47
|
+
return (
|
|
48
|
+
"connection",
|
|
49
|
+
"Cannot reach CodeMate server. Please ensure:\n"
|
|
50
|
+
" • The server is running and accessible\n"
|
|
51
|
+
" • Your network connection is stable\n"
|
|
52
|
+
" • Firewall is not blocking the connection",
|
|
53
|
+
None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Rate limit errors - parse for details
|
|
57
|
+
rate_limit_match = re.search(
|
|
58
|
+
r'rate limit exceeded.*?you have used (\d+)/(\d+) requests',
|
|
59
|
+
error_lower
|
|
60
|
+
)
|
|
61
|
+
if rate_limit_match or "rate limit" in error_lower or "ratelimiterror" in error_lower:
|
|
62
|
+
used = rate_limit_match.group(1) if rate_limit_match else "N/A"
|
|
63
|
+
limit = rate_limit_match.group(2) if rate_limit_match else "N/A"
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
"rate_limit",
|
|
67
|
+
f"Rate Limit Exceeded\n\n"
|
|
68
|
+
f"You have used {used}/{limit} requests.\n"
|
|
69
|
+
f"Please upgrade your plan for higher limits.\n\n"
|
|
70
|
+
f"Upgrade URL: {self.UPGRADE_URL}",
|
|
71
|
+
self.UPGRADE_URL
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 429 Status Code (Too Many Requests)
|
|
75
|
+
if "429" in error_msg or "too many requests" in error_lower:
|
|
76
|
+
return (
|
|
77
|
+
"rate_limit",
|
|
78
|
+
f"Too Many Requests (429)\n\n"
|
|
79
|
+
f"You've exceeded your request limit.\n"
|
|
80
|
+
f"Please upgrade your plan for higher limits.\n\n"
|
|
81
|
+
f"Upgrade URL: {self.UPGRADE_URL}",
|
|
82
|
+
self.UPGRADE_URL
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Server errors (5xx)
|
|
86
|
+
if any(code in error_msg for code in ["500", "502", "503", "504"]):
|
|
87
|
+
return (
|
|
88
|
+
"server",
|
|
89
|
+
"Internal Server Error\n\n"
|
|
90
|
+
"The CodeMate service is temporarily unavailable.\n"
|
|
91
|
+
"Please try again in a few moments.",
|
|
92
|
+
None
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Authentication errors
|
|
96
|
+
if "unauthorized" in error_lower or "401" in error_msg:
|
|
97
|
+
return (
|
|
98
|
+
"auth",
|
|
99
|
+
"Authentication Failed\n\n"
|
|
100
|
+
"Your session may have expired.\n"
|
|
101
|
+
"Please login again using /logout and restart CLI.",
|
|
102
|
+
None
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Timeout errors
|
|
106
|
+
if "timeout" in error_lower or "timed out" in error_lower:
|
|
107
|
+
return (
|
|
108
|
+
"timeout",
|
|
109
|
+
"Request Timeout\n\n"
|
|
110
|
+
"The server took too long to respond.\n"
|
|
111
|
+
"Please try again or check your connection.",
|
|
112
|
+
None
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Generic error
|
|
116
|
+
return (
|
|
117
|
+
"generic",
|
|
118
|
+
f"An error occurred:\n{error_msg}",
|
|
119
|
+
None
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def display_connection_error(self):
|
|
123
|
+
"""Display connection error panel"""
|
|
124
|
+
self.console.print()
|
|
125
|
+
self.console.print(Panel(
|
|
126
|
+
"[red]⚠️ Cannot Connect to Server[/red]\n\n"
|
|
127
|
+
"[white]Please verify:[/white]\n"
|
|
128
|
+
" [cyan]•[/cyan] Is the CodeMate server running?\n"
|
|
129
|
+
" [cyan]•[/cyan] Check your internet connection\n"
|
|
130
|
+
" [cyan]•[/cyan] Verify server endpoint configuration\n\n"
|
|
131
|
+
"[dim]Run [cyan]codemate-cli --help[/cyan] for more information[/dim]",
|
|
132
|
+
border_style="red",
|
|
133
|
+
title="[bold red]Connection Error[/bold red]",
|
|
134
|
+
padding=(1, 2),
|
|
135
|
+
box=box.ROUNDED
|
|
136
|
+
))
|
|
137
|
+
self.console.print()
|
|
138
|
+
|
|
139
|
+
def display_rate_limit_error(self, used: str = "N/A", limit: str = "N/A"):
|
|
140
|
+
"""Display rate limit error with upgrade prompt"""
|
|
141
|
+
self.console.print()
|
|
142
|
+
self.console.print(Panel(
|
|
143
|
+
f"[yellow]⚠️ Rate Limit Exceeded[/yellow]\n\n"
|
|
144
|
+
f"[white]Usage:[/white] {used}/{limit} requests\n\n"
|
|
145
|
+
f"[white]You've reached your plan's request limit.[/white]\n"
|
|
146
|
+
f"[cyan]Upgrade your plan for higher limits.[/cyan]\n\n"
|
|
147
|
+
f"[dim]Upgrade URL:[/dim] [link={self.UPGRADE_URL}]{self.UPGRADE_URL}[/link]",
|
|
148
|
+
border_style="yellow",
|
|
149
|
+
title="[bold yellow]⚡ Upgrade Required[/bold yellow]",
|
|
150
|
+
padding=(1, 2),
|
|
151
|
+
box=box.ROUNDED
|
|
152
|
+
))
|
|
153
|
+
|
|
154
|
+
# Prompt user to open browser
|
|
155
|
+
self.console.print()
|
|
156
|
+
try:
|
|
157
|
+
response = self.console.input(
|
|
158
|
+
"[cyan]Would you like to open the upgrade page in your browser? (y/n):[/cyan] "
|
|
159
|
+
).strip().lower()
|
|
160
|
+
|
|
161
|
+
if response in ['y', 'yes']:
|
|
162
|
+
try:
|
|
163
|
+
webbrowser.open(self.UPGRADE_URL)
|
|
164
|
+
self.console.print("[green]✓[/green] Opening browser...")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
self.console.print(f"[yellow]Could not open browser: {e}[/yellow]")
|
|
167
|
+
self.console.print(f"[dim]Please visit: {self.UPGRADE_URL}[/dim]")
|
|
168
|
+
except (KeyboardInterrupt, EOFError):
|
|
169
|
+
self.console.print()
|
|
170
|
+
|
|
171
|
+
self.console.print()
|
|
172
|
+
|
|
173
|
+
def display_server_error(self):
|
|
174
|
+
"""Display server error panel"""
|
|
175
|
+
self.console.print()
|
|
176
|
+
self.console.print(Panel(
|
|
177
|
+
"[red]⚠️ Internal Server Error[/red]\n\n"
|
|
178
|
+
"[white]The CodeMate service is temporarily unavailable.[/white]\n\n"
|
|
179
|
+
"[dim]Please try again in a few moments.\n"
|
|
180
|
+
"If the issue persists, contact support.[/dim]",
|
|
181
|
+
border_style="red",
|
|
182
|
+
title="[bold red]Service Unavailable[/bold red]",
|
|
183
|
+
padding=(1, 2),
|
|
184
|
+
box=box.ROUNDED
|
|
185
|
+
))
|
|
186
|
+
self.console.print()
|
|
187
|
+
|
|
188
|
+
def display_auth_error(self):
|
|
189
|
+
"""Display authentication error panel"""
|
|
190
|
+
self.console.print()
|
|
191
|
+
self.console.print(Panel(
|
|
192
|
+
"[red]⚠️ Authentication Failed[/red]\n\n"
|
|
193
|
+
"[white]Your session may have expired.[/white]\n\n"
|
|
194
|
+
"[cyan]Please run:[/cyan] [bold]/logout[/bold]\n"
|
|
195
|
+
"[dim]Then restart the CLI to login again.[/dim]",
|
|
196
|
+
border_style="red",
|
|
197
|
+
title="[bold red]Session Expired[/bold red]",
|
|
198
|
+
padding=(1, 2),
|
|
199
|
+
box=box.ROUNDED
|
|
200
|
+
))
|
|
201
|
+
self.console.print()
|
|
202
|
+
|
|
203
|
+
def display_timeout_error(self):
|
|
204
|
+
"""Display timeout error panel"""
|
|
205
|
+
self.console.print()
|
|
206
|
+
self.console.print(Panel(
|
|
207
|
+
"[yellow]⚠️ Request Timeout[/yellow]\n\n"
|
|
208
|
+
"[white]The server took too long to respond.[/white]\n\n"
|
|
209
|
+
"[dim]Please try again or check your connection.[/dim]",
|
|
210
|
+
border_style="yellow",
|
|
211
|
+
title="[bold yellow]Timeout[/bold yellow]",
|
|
212
|
+
padding=(1, 2),
|
|
213
|
+
box=box.ROUNDED
|
|
214
|
+
))
|
|
215
|
+
self.console.print()
|
|
216
|
+
|
|
217
|
+
def display_generic_error(self, error_msg: str):
|
|
218
|
+
"""Display generic error panel"""
|
|
219
|
+
self.console.print()
|
|
220
|
+
self.console.print(Panel(
|
|
221
|
+
f"[red]❌ Error[/red]\n\n"
|
|
222
|
+
f"[white]{error_msg}[/white]\n\n"
|
|
223
|
+
f"[dim]If this issue persists, please contact support.[/dim]",
|
|
224
|
+
border_style="red",
|
|
225
|
+
title="[bold red]Error[/bold red]",
|
|
226
|
+
padding=(1, 2),
|
|
227
|
+
box=box.ROUNDED
|
|
228
|
+
))
|
|
229
|
+
self.console.print()
|
|
230
|
+
|
|
231
|
+
def handle_error(self, error: Exception):
|
|
232
|
+
"""Main error handler - routes to appropriate display method"""
|
|
233
|
+
error_msg = str(error)
|
|
234
|
+
error_type, display_msg, upgrade_url = self.parse_error_message(error_msg)
|
|
235
|
+
|
|
236
|
+
if error_type == "connection":
|
|
237
|
+
self.display_connection_error()
|
|
238
|
+
elif error_type == "rate_limit":
|
|
239
|
+
# Extract usage numbers if available
|
|
240
|
+
match = re.search(r'(\d+)/(\d+) requests', error_msg)
|
|
241
|
+
if match:
|
|
242
|
+
self.display_rate_limit_error(match.group(1), match.group(2))
|
|
243
|
+
else:
|
|
244
|
+
self.display_rate_limit_error()
|
|
245
|
+
elif error_type == "server":
|
|
246
|
+
self.display_server_error()
|
|
247
|
+
elif error_type == "auth":
|
|
248
|
+
self.display_auth_error()
|
|
249
|
+
elif error_type == "timeout":
|
|
250
|
+
self.display_timeout_error()
|
|
251
|
+
else:
|
|
252
|
+
self.display_generic_error(display_msg)
|
|
253
|
+
|
|
254
|
+
def handle_streaming_error(self, error_data: dict):
|
|
255
|
+
"""Handle errors from streaming API (type: "error" chunks)"""
|
|
256
|
+
error_msg = error_data.get("message", "Unknown error occurred")
|
|
257
|
+
|
|
258
|
+
# Parse the error
|
|
259
|
+
error_type, display_msg, upgrade_url = self.parse_error_message(error_msg)
|
|
260
|
+
|
|
261
|
+
# Display appropriate error
|
|
262
|
+
if error_type == "rate_limit":
|
|
263
|
+
match = re.search(r'(\d+)/(\d+) requests', error_msg)
|
|
264
|
+
if match:
|
|
265
|
+
self.display_rate_limit_error(match.group(1), match.group(2))
|
|
266
|
+
else:
|
|
267
|
+
self.display_rate_limit_error()
|
|
268
|
+
elif error_type == "server":
|
|
269
|
+
self.display_server_error()
|
|
270
|
+
elif error_type == "auth":
|
|
271
|
+
self.display_auth_error()
|
|
272
|
+
elif error_type == "connection":
|
|
273
|
+
self.display_connection_error()
|
|
274
|
+
elif error_type == "timeout":
|
|
275
|
+
self.display_timeout_error()
|
|
276
|
+
else:
|
|
277
|
+
self.display_generic_error(error_msg)
|
codemate/utils/errors.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CodeMateError(Exception):
|
|
12
|
+
"""Base exception for CodeMate CLI"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIError(CodeMateError):
|
|
17
|
+
"""API-related errors"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigError(CodeMateError):
|
|
22
|
+
"""Configuration-related errors"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AuthenticationError(APIError):
|
|
27
|
+
"""Authentication failed"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NetworkError(APIError):
|
|
32
|
+
"""Network connectivity issues"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def handle_errors(func: Callable) -> Callable:
|
|
37
|
+
"""Decorator to handle errors gracefully in CLI commands"""
|
|
38
|
+
|
|
39
|
+
@functools.wraps(func)
|
|
40
|
+
def wrapper(*args, **kwargs):
|
|
41
|
+
try:
|
|
42
|
+
return func(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
except httpx.HTTPStatusError as e:
|
|
45
|
+
if e.response.status_code == 401:
|
|
46
|
+
console.print(Panel(
|
|
47
|
+
"[red]Authentication failed![/red]\n\n"
|
|
48
|
+
"Your API key may be invalid or expired.\n"
|
|
49
|
+
"Please set a valid API key:\n\n"
|
|
50
|
+
"[cyan]codemate config set-key YOUR_API_KEY[/cyan]",
|
|
51
|
+
title="[bold red]Authentication Error[/bold red]",
|
|
52
|
+
border_style="red"
|
|
53
|
+
))
|
|
54
|
+
elif e.response.status_code == 429:
|
|
55
|
+
console.print(Panel(
|
|
56
|
+
"[red]Rate limit exceeded![/red]\n\n"
|
|
57
|
+
"You've made too many requests. Please wait a moment and try again.",
|
|
58
|
+
title="[bold red]Rate Limit[/bold red]",
|
|
59
|
+
border_style="red"
|
|
60
|
+
))
|
|
61
|
+
elif e.response.status_code == 500:
|
|
62
|
+
console.print(Panel(
|
|
63
|
+
"[red]Server error![/red]\n\n"
|
|
64
|
+
"The API server encountered an error.\n"
|
|
65
|
+
"Please try again later or contact support.",
|
|
66
|
+
title="[bold red]Server Error[/bold red]",
|
|
67
|
+
border_style="red"
|
|
68
|
+
))
|
|
69
|
+
else:
|
|
70
|
+
console.print(Panel(
|
|
71
|
+
f"[red]HTTP Error {e.response.status_code}[/red]\n\n"
|
|
72
|
+
f"{str(e)}",
|
|
73
|
+
title="[bold red]Request Failed[/bold red]",
|
|
74
|
+
border_style="red"
|
|
75
|
+
))
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
except httpx.ConnectError:
|
|
79
|
+
console.print(Panel(
|
|
80
|
+
"[red]Connection failed![/red]\n\n"
|
|
81
|
+
"Could not connect to the API server.\n"
|
|
82
|
+
"Please check:\n"
|
|
83
|
+
" • Is the server running?\n"
|
|
84
|
+
" • Is the endpoint correct?\n"
|
|
85
|
+
" • Do you have network connectivity?\n\n"
|
|
86
|
+
f"Current endpoint: [cyan]{kwargs.get('endpoint', 'http://localhost:45223')}[/cyan]\n\n"
|
|
87
|
+
"Set a different endpoint:\n"
|
|
88
|
+
"[cyan]codemate config set-endpoint http://your-server:port[/cyan]",
|
|
89
|
+
title="[bold red]Connection Error[/bold red]",
|
|
90
|
+
border_style="red"
|
|
91
|
+
))
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
except httpx.TimeoutException:
|
|
95
|
+
console.print(Panel(
|
|
96
|
+
"[red]Request timed out![/red]\n\n"
|
|
97
|
+
"The request took too long to complete.\n"
|
|
98
|
+
"This might be due to:\n"
|
|
99
|
+
" • Slow network connection\n"
|
|
100
|
+
" • Server overload\n"
|
|
101
|
+
" • Large request processing\n\n"
|
|
102
|
+
"Please try again.",
|
|
103
|
+
title="[bold red]Timeout Error[/bold red]",
|
|
104
|
+
border_style="red"
|
|
105
|
+
))
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
except ConfigError as e:
|
|
109
|
+
console.print(Panel(
|
|
110
|
+
f"[red]Configuration error![/red]\n\n{str(e)}",
|
|
111
|
+
title="[bold red]Config Error[/bold red]",
|
|
112
|
+
border_style="red"
|
|
113
|
+
))
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
|
|
116
|
+
except KeyboardInterrupt:
|
|
117
|
+
console.print("\n[yellow]Operation cancelled by user.[/yellow]")
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
console.print(Panel(
|
|
122
|
+
f"[red]Unexpected error![/red]\n\n"
|
|
123
|
+
f"{type(e).__name__}: {str(e)}\n\n"
|
|
124
|
+
"If this persists, please report the issue.",
|
|
125
|
+
title="[bold red]Error[/bold red]",
|
|
126
|
+
border_style="red"
|
|
127
|
+
))
|
|
128
|
+
if console.is_terminal:
|
|
129
|
+
import traceback
|
|
130
|
+
console.print("\n[dim]Full traceback:[/dim]")
|
|
131
|
+
traceback.print_exc()
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
return wrapper
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def format_error_message(error: Exception) -> str:
|
|
138
|
+
"""Format an error message for display"""
|
|
139
|
+
error_type = type(error).__name__
|
|
140
|
+
return f"{error_type}: {str(error)}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def is_network_error(error: Exception) -> bool:
|
|
144
|
+
"""Check if an error is network-related"""
|
|
145
|
+
return isinstance(error, (
|
|
146
|
+
httpx.ConnectError,
|
|
147
|
+
httpx.TimeoutException,
|
|
148
|
+
httpx.NetworkError,
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def is_auth_error(error: Exception) -> bool:
|
|
153
|
+
"""Check if an error is authentication-related"""
|
|
154
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
155
|
+
return error.response.status_code == 401
|
|
156
|
+
return isinstance(error, AuthenticationError)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for parsing and handling knowledge base mentions in user input
|
|
3
|
+
"""
|
|
4
|
+
import re
|
|
5
|
+
from typing import Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_kb_mentions(user_input: str, available_kbs: List[Dict]) -> Tuple[str, List[Dict], List[str]]:
|
|
9
|
+
"""
|
|
10
|
+
Parse @kb_name mentions from user input and build context
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
user_input: The user's input message
|
|
14
|
+
available_kbs: List of available knowledge bases
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Tuple of (modified_message, context_list, invalid_kb_names)
|
|
18
|
+
- modified_message: Message with KB context XML tags inserted
|
|
19
|
+
- context_list: List of context objects for API
|
|
20
|
+
- invalid_kb_names: List of KB names that were mentioned but not found
|
|
21
|
+
"""
|
|
22
|
+
# Find all @mentions in the input (supporting hyphens, dots, and word characters)
|
|
23
|
+
mentions = re.findall(r'@([\w.-]+)', user_input)
|
|
24
|
+
|
|
25
|
+
if not mentions:
|
|
26
|
+
return user_input, [], []
|
|
27
|
+
|
|
28
|
+
# Create a mapping of KB names to KB objects
|
|
29
|
+
kb_map = {kb['name']: kb for kb in available_kbs}
|
|
30
|
+
|
|
31
|
+
context_list = []
|
|
32
|
+
invalid_kbs = []
|
|
33
|
+
modified_message = user_input
|
|
34
|
+
|
|
35
|
+
# Process each mention
|
|
36
|
+
for kb_name in mentions:
|
|
37
|
+
if kb_name in kb_map:
|
|
38
|
+
kb = kb_map[kb_name]
|
|
39
|
+
|
|
40
|
+
# Build context object
|
|
41
|
+
context_obj = {
|
|
42
|
+
"type": kb.get("type", "codebase"),
|
|
43
|
+
"kbid": kb["id"],
|
|
44
|
+
"name": kb["name"],
|
|
45
|
+
"id": kb["id"]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Only add if not already in context list
|
|
49
|
+
if not any(ctx["id"] == context_obj["id"] for ctx in context_list):
|
|
50
|
+
context_list.append(context_obj)
|
|
51
|
+
|
|
52
|
+
# Build XML context tag
|
|
53
|
+
context_xml = (
|
|
54
|
+
f"<cm:context>"
|
|
55
|
+
f"<cm:context:name>{kb['name']}</cm:context:name>"
|
|
56
|
+
f"<cm:context:type>{kb.get('type', 'codebase')}</cm:context:type>"
|
|
57
|
+
f"<cm:context:id>{kb['id']}</cm:context:id>"
|
|
58
|
+
f"<cm:context:content></cm:context:content>"
|
|
59
|
+
f"</cm:context>"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Replace the @mention with context XML + @mention
|
|
63
|
+
# This ensures the mention appears once in the message
|
|
64
|
+
pattern = f"@{kb_name}"
|
|
65
|
+
if pattern in modified_message:
|
|
66
|
+
modified_message = modified_message.replace(
|
|
67
|
+
pattern,
|
|
68
|
+
f"{context_xml} ",
|
|
69
|
+
1 # Replace only first occurrence
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
invalid_kbs.append(kb_name)
|
|
73
|
+
|
|
74
|
+
# Remove any remaining @mentions that were invalid
|
|
75
|
+
for invalid_kb in invalid_kbs:
|
|
76
|
+
modified_message = modified_message.replace(f"@{invalid_kb}", f"[Invalid KB: @{invalid_kb}]")
|
|
77
|
+
|
|
78
|
+
return modified_message, context_list, invalid_kbs
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_kb_context_message(
|
|
82
|
+
user_input: str,
|
|
83
|
+
available_kbs: List[Dict]
|
|
84
|
+
) -> Tuple[Optional[str], Optional[List[Dict]], Optional[str]]:
|
|
85
|
+
"""
|
|
86
|
+
Format user input with KB context for API
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
user_input: The user's input message
|
|
90
|
+
available_kbs: List of available knowledge bases
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Tuple of (message, context, error_message)
|
|
94
|
+
- If successful: (formatted_message, context_list, None)
|
|
95
|
+
- If error: (None, None, error_message)
|
|
96
|
+
"""
|
|
97
|
+
modified_message, context_list, invalid_kbs = parse_kb_mentions(user_input, available_kbs)
|
|
98
|
+
|
|
99
|
+
# If there are invalid KB names, return error
|
|
100
|
+
if invalid_kbs:
|
|
101
|
+
error_msg = (
|
|
102
|
+
f"Invalid knowledge base name(s): {', '.join(['@' + kb for kb in invalid_kbs])}\n\n"
|
|
103
|
+
f"Use /listkb to see available knowledge bases."
|
|
104
|
+
)
|
|
105
|
+
return None, None, error_msg
|
|
106
|
+
|
|
107
|
+
# If no context found, return original message with empty context
|
|
108
|
+
if not context_list:
|
|
109
|
+
return user_input, None, None
|
|
110
|
+
|
|
111
|
+
return modified_message, context_list, None
|