Coverage for src / osiris_cli / cli.py: 0%

208 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-27 17:41 +0200

1#!/usr/bin/env python3 

2""" 

3Osiris CLI - Rich Console Interface 

4The working functional CLI that was lost 

5""" 

6 

7from rich.console import Console 

8from rich.panel import Panel 

9from rich.prompt import Prompt 

10from rich.live import Live 

11from rich.table import Table 

12from rich.layout import Layout 

13from rich.align import Align 

14from rich.text import Text 

15from rich.spinner import Spinner 

16from rich.markdown import Markdown 

17from rich.columns import Columns 

18import asyncio 

19import json 

20import os 

21import sys 

22from pathlib import Path 

23 

24from .config import settings 

25from .enhanced_commands import enhanced_app 

26from .client import async_client 

27from .context import context 

28from .session import session 

29from .tools import tools 

30 

31console = Console() 

32 

33class OsirisCLI: 

34 """Rich console-based CLI interface""" 

35 

36 def __init__(self): 

37 self.running = True 

38 

39 def show_welcome(self): 

40 """Show welcome banner""" 

41 welcome_panel = Panel( 

42 f"[bold cyan]👁️ OSIRIS CLI v3.2.0[/bold cyan]\n\n" 

43 f"[bold white]Provider:[/bold white] {settings.provider.upper()}\n" 

44 f"[bold white]Model:[/bold white] {settings.default_model}\n\n" 

45 "[dim]Type /help for commands, or just chat naturally[/dim]\n" 

46 "[dim]Press Ctrl+C to exit[/dim]", 

47 title="🚀 Sovereign AGI Terminal", 

48 border_style="cyan" 

49 ) 

50 console.print(welcome_panel) 

51 

52 def show_help(self): 

53 """Show available commands""" 

54 help_table = Table(title="🎯 Available Commands") 

55 help_table.add_column("Command", style="cyan", width=15) 

56 help_table.add_column("Description", style="white") 

57 help_table.add_column("Example", style="dim") 

58 

59 commands = [ 

60 ("/help", "Show this help", "/help"), 

61 ("/clear", "Clear conversation", "/clear"), 

62 ("/save", "Save session", "/save"), 

63 ("/load", "Load session", "/load"), 

64 ("/status", "Show system status", "/status"), 

65 ("/config", "Show configuration", "/config"), 

66 ("/context", "Manage context files", "/context add file.py"), 

67 ("/tools", "List available tools", "/tools"), 

68 ("/exit", "Exit the CLI", "/exit"), 

69 ] 

70 

71 for cmd, desc, example in commands: 

72 help_table.add_row(cmd, desc, example) 

73 

74 console.print(help_table) 

75 

76 def handle_command(self, command: str): 

77 """Handle slash commands""" 

78 parts = command.strip().split() 

79 cmd = parts[0].lower() 

80 

81 if cmd == "/help": 

82 self.show_help() 

83 elif cmd == "/clear": 

84 session.clear() 

85 context.clear() 

86 console.print("[green]🧹 Session and context cleared[/green]") 

87 elif cmd == "/save": 

88 if session.save_session(): 

89 console.print("[green]💾 Session saved[/green]") 

90 else: 

91 console.print("[red]❌ Failed to save session[/red]") 

92 elif cmd == "/load": 

93 if session.load_last_session(): 

94 console.print("[green]📂 Session loaded[/green]") 

95 else: 

96 console.print("[yellow]⚠️ No previous session found[/yellow]") 

97 elif cmd == "/status": 

98 self.show_status() 

99 elif cmd == "/config": 

100 self.show_config() 

101 elif cmd == "/context": 

102 self.handle_context_command(parts[1:]) 

103 elif cmd == "/tools": 

104 self.show_tools() 

105 elif cmd == "/exit": 

106 self.running = False 

107 console.print("[yellow]👋 Goodbye![/yellow]") 

108 else: 

109 console.print(f"[red]❓ Unknown command: {cmd}[/red]") 

110 console.print("[dim]Type /help for available commands[/dim]") 

111 

112 def show_status(self): 

113 """Show system status""" 

114 stats = session.get_session_stats() 

115 

116 status_table = Table(title="📊 System Status") 

117 status_table.add_column("Component", style="cyan") 

118 status_table.add_column("Status", style="green") 

119 status_table.add_column("Details", style="white") 

120 

121 status_table.add_row("Version", "✅ Active", "3.2.0") 

122 status_table.add_row("Provider", "🔗 Connected", settings.provider.upper()) 

123 status_table.add_row("Model", "🤖 Ready", settings.default_model) 

124 status_table.add_row("Sessions", "💾 Available", f"{stats.get('total_sessions', 0)}") 

125 status_table.add_row("Context Files", "📄 Loaded", f"{len(context.loaded_files)}") 

126 status_table.add_row("Tokens Used", "🔢 Total", f"{stats.get('total_tokens', 0):,}") 

127 

128 console.print(status_table) 

129 

130 def show_config(self): 

131 """Show current configuration""" 

132 config_table = Table(title="⚙️ Configuration") 

133 config_table.add_column("Setting", style="cyan") 

134 config_table.add_column("Value", style="white") 

135 

136 config_table.add_row("Provider", settings.provider) 

137 config_table.add_row("Model", settings.default_model) 

138 config_table.add_row("Temperature", str(settings.temperature)) 

139 config_table.add_row("Max Tokens", "Default") 

140 config_table.add_row("Timeout", f"{settings.timeout}s") 

141 config_table.add_row("Theme", settings.theme) 

142 config_table.add_row("YOLO Mode", "Enabled" if settings.yolo_mode else "Disabled") 

143 

144 console.print(config_table) 

145 

146 def handle_context_command(self, args): 

147 """Handle context-related commands""" 

148 if not args: 

149 # Show current context 

150 if not context.loaded_files: 

151 console.print("[yellow]📄 No context files loaded[/yellow]") 

152 return 

153 

154 context_table = Table(title="📄 Context Files") 

155 context_table.add_column("File", style="cyan") 

156 context_table.add_column("Size", style="white", justify="right") 

157 

158 for file_path, content in context.loaded_files.items(): 

159 size = len(str(content)) 

160 context_table.add_row(file_path, f"{size:,} chars") 

161 

162 console.print(context_table) 

163 return 

164 

165 action = args[0].lower() 

166 if action == "add" and len(args) > 1: 

167 file_path = args[1] 

168 if context.add_file(file_path): 

169 console.print(f"[green]✅ Added to context: {file_path}[/green]") 

170 else: 

171 console.print(f"[red]❌ Failed to add: {file_path}[/red]") 

172 elif action == "remove" and len(args) > 1: 

173 file_path = args[1] 

174 if context.remove_file(file_path): 

175 console.print(f"[green]✅ Removed from context: {file_path}[/green]") 

176 else: 

177 console.print(f"[red]❌ File not in context: {file_path}[/red]") 

178 elif action == "clear": 

179 context.clear() 

180 console.print("[green]🧹 Context cleared[/green]") 

181 else: 

182 console.print("[red]❓ Usage: /context [add|remove|clear] [file][/red]") 

183 

184 def show_tools(self): 

185 """Show available tools""" 

186 if not tools.tools: 

187 console.print("[yellow]🛠️ No tools available[/yellow]") 

188 return 

189 

190 tools_table = Table(title="🛠️ Available Tools") 

191 tools_table.add_column("Tool", style="cyan") 

192 tools_table.add_column("Description", style="white") 

193 

194 for tool_name, tool_info in tools.tools.items(): 

195 tools_table.add_row(tool_name, tool_info.get("description", "No description")) 

196 

197 console.print(tools_table) 

198 

199 async def process_message(self, message: str): 

200 """Process a user message through the AI""" 

201 try: 

202 # Add to session 

203 session.add_message("user", message) 

204 

205 # Prepare system prompt 

206 sys_prompt = settings.system_prompt 

207 if settings.reasoning_mode: 

208 sys_prompt += "\n[REASONING ENABLED]" 

209 if context.loaded_files: 

210 sys_prompt += context.get_system_prompt_addition() 

211 

212 if not session.messages: 

213 session.messages.append({"role": "system", "content": sys_prompt}) 

214 

215 # Get tool definitions 

216 t_defs = [{"type": "function", "function": t["function"]} for t in tools.tools.values()] 

217 

218 # Process with AI 

219 console.print() # Add spacing 

220 

221 with Live(console=console, refresh_per_second=4) as live: 

222 accumulated_response = "" 

223 tool_calls_made = [] 

224 

225 for attempt in range(5): 

226 if not self.running: 

227 break 

228 

229 response = await async_client.chat(session.messages, tools=t_defs) 

230 

231 if isinstance(response, str): 

232 # Error response 

233 error_panel = Panel( 

234 f"[red]❌ Error: {response}[/red]", 

235 title="🔥 AI Error", 

236 border_style="red" 

237 ) 

238 console.print(error_panel) 

239 return 

240 

241 msg = response.choices[0].message 

242 content = msg.content or "" 

243 tool_calls = msg.tool_calls or [] 

244 

245 if tool_calls: 

246 # First, add the assistant message with tool_calls to session 

247 if content: 

248 session.add_message("assistant", content, tool_calls=tool_calls) 

249 else: 

250 session.add_message("assistant", "", tool_calls=tool_calls) 

251 

252 # Handle tool calls 

253 for tc in tool_calls: 

254 fname = tc.function.name 

255 args = json.loads(tc.function.arguments) 

256 

257 # Show tool call 

258 tool_panel = Panel( 

259 f"[cyan]🔧 Executing: {fname}[/cyan]\n" 

260 f"[dim]Args: {args}[/dim]", 

261 title="🛠️ Tool Call", 

262 border_style="blue" 

263 ) 

264 live.update(tool_panel) 

265 

266 try: 

267 func = tools.tools[fname]["implementation"] 

268 if asyncio.iscoroutinefunction(func): 

269 result = str(await func(**args)) 

270 else: 

271 result = str(await asyncio.to_thread(func, **args)) 

272 except Exception as e: 

273 result = f"Error: {e}" 

274 

275 # Show tool result 

276 result_panel = Panel( 

277 f"[green]✅ {fname} completed[/green]\n\n" 

278 f"[white]{result[:500]}{'...' if len(result) > 500 else ''}[/white]", 

279 title="📋 Tool Result", 

280 border_style="green" 

281 ) 

282 live.update(result_panel) 

283 

284 session.add_tool_output(tc.id, fname, result) 

285 

286 # Continue the conversation 

287 continue 

288 else: 

289 # Final response 

290 if content: 

291 # Display response 

292 response_panel = Panel( 

293 Markdown(content), 

294 title="[bold cyan]Osiris[/bold cyan]", 

295 border_style="cyan" 

296 ) 

297 live.update(response_panel) 

298 session.add_message("assistant", content) 

299 break 

300 

301 except Exception as e: 

302 error_panel = Panel( 

303 f"[red]❌ Processing error: {e}[/red]", 

304 title="🔥 Error", 

305 border_style="red" 

306 ) 

307 console.print(error_panel) 

308 

309 def run(self): 

310 """Main CLI loop""" 

311 self.show_welcome() 

312 

313 while self.running: 

314 try: 

315 # Get user input 

316 user_input = Prompt.ask("\n[bold cyan]You[/bold cyan]").strip() 

317 

318 if not user_input: 

319 continue 

320 

321 if user_input.startswith("/"): 

322 # Handle command 

323 self.handle_command(user_input) 

324 else: 

325 # Process as AI message 

326 console.print(f"[dim]🤖 Processing: {user_input}[/dim]") 

327 

328 # Run async processing 

329 asyncio.run(self.process_message(user_input)) 

330 

331 except KeyboardInterrupt: 

332 console.print("\n[yellow]👋 Goodbye![/yellow]") 

333 break 

334 except EOFError: 

335 break 

336 except Exception as e: 

337 console.print(f"[red]❌ Error: {e}[/red]") 

338 

339def run_cli(): 

340 """Entry point for the CLI""" 

341 cli = OsirisCLI() 

342 cli.run() 

343 

344if __name__ == "__main__": 

345 run_cli()