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

188 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 v5.1 - Enhanced UX 

4Blends Gemini CLI's clean streaming with Letta Code's structured display 

5""" 

6 

7import asyncio 

8import time 

9from pathlib import Path 

10from typing import Optional 

11from rich.console import Console 

12from rich.markdown import Markdown 

13 

14from .chat_engine import ChatEngine, StreamingState, ToolCallStatus 

15from .tools_registry import tools_registry 

16from .config import settings 

17from .client import get_async_client 

18from .enhanced_display import ( 

19 StreamingDisplay, 

20 ToolDisplay, 

21 ThoughtDisplay, 

22 MemoryDisplay, 

23 SessionDisplay, 

24 AgentSelector 

25) 

26 

27console = Console() 

28 

29 

30class OsirisCLI_Enhanced: 

31 """ 

32 Enhanced CLI with Gemini + Letta UX patterns 

33 """ 

34 

35 def __init__(self, agent_name: str = "main"): 

36 self.agent_name = agent_name 

37 self.running = True 

38 self.session_start = time.time() 

39 self.tools_used = 0 

40 self.files_modified = [] 

41 self.key_actions = [] 

42 

43 # Initialize chat engine 

44 self.chat_engine = ChatEngine( 

45 client=get_async_client(), 

46 settings=settings, 

47 tools_registry=tools_registry, 

48 max_tool_calls_per_turn=50, 

49 enable_auto_approval=settings.yolo_mode 

50 ) 

51 

52 # Load agent memory/context 

53 self.system_context, self.memory = self._load_system_context() 

54 

55 # Display system 

56 self.stream_display = StreamingDisplay() 

57 self.tool_display = ToolDisplay() 

58 

59 def _load_system_context(self) -> tuple[str, dict]: 

60 """Load system context and return both context string and memory dict""" 

61 context_parts = [] 

62 memory = {} 

63 

64 # Add base system prompt 

65 if settings.system_prompt: 

66 context_parts.append(settings.system_prompt) 

67 

68 # Load OSIRIS.md if it exists 

69 osiris_md = Path.cwd() / "OSIRIS.md" 

70 if osiris_md.exists(): 

71 with open(osiris_md, 'r') as f: 

72 context_parts.append(f"# Project Context\n{f.read()}") 

73 

74 # Load agent memory 

75 memory_file = Path.home() / ".osiris" / "agents" / f"{self.agent_name}.json" 

76 if memory_file.exists(): 

77 import json 

78 try: 

79 with open(memory_file, 'r') as f: 

80 memory = json.load(f) 

81 if memory.get("core_memory"): 

82 mem_str = "\n".join([f"- {k}: {v}" for k, v in memory["core_memory"].items()]) 

83 context_parts.append(f"# Agent Memory\n{mem_str}") 

84 except: 

85 pass 

86 

87 return "\n\n".join(context_parts), memory 

88 

89 def show_welcome(self): 

90 """Display welcome with memory card""" 

91 # Show memory card if available 

92 if self.memory: 

93 MemoryDisplay.show_memory_card(self.agent_name, self.memory) 

94 console.print() 

95 

96 # Welcome message 

97 welcome = f""" 

98# Osiris CLI v5.1 

99 

100**Agent**: {self.agent_name} 

101**Model**: {settings.default_model} 

102**Mode**: {'YOLO (autonomous)' if settings.yolo_mode else 'Interactive'} 

103 

104Type `/help` for commands or start chatting. 

105 """ 

106 console.print(Markdown(welcome)) 

107 console.print() 

108 

109 async def handle_message(self, user_input: str): 

110 """Handle message with enhanced display""" 

111 console.print("\n[bold]Response:[/bold]\n") 

112 

113 tool_count = 0 

114 response_text = "" 

115 tool_start_time = None 

116 

117 try: 

118 async for event in self.chat_engine.send_message( 

119 user_input, 

120 system_context=self.system_context 

121 ): 

122 event_type = event["type"] 

123 

124 if event_type == "content": 

125 # Stream text character-by-character (Gemini-style) 

126 text = event["text"] 

127 response_text += text 

128 console.print(text, end="", markup=False) 

129 

130 elif event_type == "tool_call": 

131 # Clean tool display (Gemini-inspired) 

132 tool = event["tool"] 

133 tool_count += 1 

134 self.tools_used += 1 

135 

136 console.print() # Newline before tool 

137 self.tool_display.show_tool_call(tool.name, tool.arguments, compact=True) 

138 tool_start_time = time.time() 

139 

140 elif event_type == "tool_result": 

141 # Show result with duration 

142 tool = event["tool"] 

143 duration = time.time() - tool_start_time if tool_start_time else None 

144 

145 if tool.status == ToolCallStatus.SUCCESS: 

146 self.tool_display.show_tool_result( 

147 tool.result or "", 

148 success=True, 

149 duration=duration 

150 ) 

151 

152 # Track file modifications 

153 if tool.name in ["write_file", "replace_file_content"]: 

154 if "path" in tool.arguments: 

155 self.files_modified.append(tool.arguments["path"]) 

156 else: 

157 self.tool_display.show_tool_result( 

158 tool.error or "Unknown error", 

159 success=False, 

160 duration=duration 

161 ) 

162 

163 console.print() # Newline after tool 

164 

165 elif event_type == "error": 

166 console.print(f"\n[red]Error:[/red] {event['message']}") 

167 

168 console.print("\n") 

169 

170 # Track key action 

171 if tool_count > 0: 

172 self.key_actions.append(f"Used {tool_count} tools to: {user_input[:50]}...") 

173 

174 except KeyboardInterrupt: 

175 console.print("\n\n[yellow]⚠ Interrupted by user[/yellow]\n") 

176 except Exception as e: 

177 console.print(f"\n[red]Error:[/red] {e}\n") 

178 

179 async def handle_slash_command(self, command: str) -> bool: 

180 """Handle slash commands""" 

181 parts = command.split(maxsplit=1) 

182 cmd = parts[0].lower() 

183 args = parts[1] if len(parts) > 1 else "" 

184 

185 if cmd == "/help": 

186 self.show_help() 

187 return True 

188 

189 elif cmd in ["/exit", "/quit"]: 

190 self.show_session_summary() 

191 self.running = False 

192 return True 

193 

194 elif cmd == "/clear": 

195 import os 

196 os.system('clear' if os.name != 'nt' else 'cls') 

197 return True 

198 

199 elif cmd == "/reset": 

200 self.chat_engine.reset() 

201 console.print("[green]✓[/green] Conversation reset\n") 

202 return True 

203 

204 elif cmd == "/memory": 

205 # Show memory card 

206 if self.memory: 

207 MemoryDisplay.show_memory_card(self.agent_name, self.memory) 

208 else: 

209 console.print("[yellow]No memory stored yet[/yellow]\n") 

210 return True 

211 

212 elif cmd == "/yolo": 

213 settings.yolo_mode = not settings.yolo_mode 

214 settings.save() 

215 status = "enabled" if settings.yolo_mode else "disabled" 

216 console.print(f"[green]✓[/green] YOLO mode {status}\n") 

217 return True 

218 

219 elif cmd == "/model": 

220 from .live_menus import select_model 

221 selected = await select_model() 

222 if selected: 

223 get_async_client().reinitialize() 

224 console.print(f"\n[green]✓[/green] Model changed to: {settings.default_model}\n") 

225 return True 

226 

227 elif cmd == "/provider": 

228 from .live_menus import select_provider 

229 selected = await select_provider() 

230 if selected: 

231 get_async_client().reinitialize() 

232 console.print(f"\n[green]✓[/green] Provider changed to: {settings.provider}\n") 

233 return True 

234 

235 elif cmd == "/history": 

236 conversation = self.chat_engine.get_conversation() 

237 if not conversation: 

238 console.print("[yellow]No conversation history yet[/yellow]\n") 

239 else: 

240 console.print(f"\n[bold]Conversation History ({len(conversation)} messages):[/bold]\n") 

241 for msg in conversation[-10:]: 

242 role = msg["role"] 

243 content = str(msg.get("content", ""))[:100] 

244 console.print(f" [{role}] {content}...") 

245 console.print() 

246 return True 

247 

248 elif cmd == "/tools": 

249 tools = tools_registry.list_tools() 

250 console.print(f"\n[bold]Available Tools ({len(tools)}):[/bold]\n") 

251 for tool in tools: 

252 console.print(f" • [cyan]{tool.name}[/cyan]: {tool.description}") 

253 console.print() 

254 return True 

255 

256 elif cmd == "/summary": 

257 self.show_session_summary() 

258 return True 

259 

260 return False 

261 

262 def show_help(self): 

263 """Show help""" 

264 help_text = """ 

265# Osiris CLI v5.1 - Command Reference 

266 

267## Core Commands 

268- `/help` - Show this help 

269- `/exit` - Exit Osiris (shows session summary) 

270- `/clear` - Clear screen 

271- `/reset` - Reset conversation 

272 

273## Configuration 

274- `/model` - Change AI model 

275- `/provider` - Change provider 

276- `/yolo` - Toggle autonomous mode 

277 

278## Information 

279- `/history` - Show conversation history 

280- `/tools` - List available tools 

281- `/memory` - Show agent memory 

282- `/summary` - Show session summary 

283 

284## Features 

285- **Autonomous Tool Execution**: No round limits 

286- **Streaming Display**: Text streams smoothly 

287- **Clean Tool Output**: Gemini-inspired formatting 

288- **Memory Persistence**: Letta-style agent memory 

289- **Session Summaries**: Auto-generated on exit 

290 """ 

291 console.print(Markdown(help_text)) 

292 console.print() 

293 

294 def show_session_summary(self): 

295 """Show session summary (Gemini-style)""" 

296 duration_seconds = int(time.time() - self.session_start) 

297 duration_str = f"{duration_seconds // 60}m {duration_seconds % 60}s" 

298 

299 SessionDisplay.show_summary( 

300 duration=duration_str, 

301 tools_used=self.tools_used, 

302 files_modified=len(set(self.files_modified)), 

303 key_actions=self.key_actions[-5:] # Last 5 actions 

304 ) 

305 

306 async def run(self): 

307 """Main CLI loop""" 

308 self.show_welcome() 

309 

310 while self.running: 

311 try: 

312 user_input = await asyncio.to_thread( 

313 input, 

314 "\n[bold cyan]You:[/bold cyan] " 

315 ) 

316 

317 if not user_input.strip(): 

318 continue 

319 

320 if user_input.startswith("/"): 

321 if await self.handle_slash_command(user_input): 

322 continue 

323 

324 await self.handle_message(user_input) 

325 

326 except KeyboardInterrupt: 

327 console.print("\n[yellow]Use /exit to quit[/yellow]\n") 

328 continue 

329 except EOFError: 

330 self.show_session_summary() 

331 break 

332 

333 console.print("\n[cyan]Goodbye![/cyan]\n") 

334 

335 

336async def main(agent_name: str = "main", prompt: Optional[str] = None): 

337 """Entry point for enhanced CLI""" 

338 cli = OsirisCLI_Enhanced(agent_name=agent_name) 

339 

340 if prompt: 

341 await cli.handle_message(prompt) 

342 cli.show_session_summary() 

343 else: 

344 await cli.run()