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

158 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.0 - Rebuilt from scratch 

4Based on Gemini CLI and Letta Code architectures. 

5 

6Complete rewrite of the chat/agent loop to match proven agentic patterns. 

7""" 

8 

9import asyncio 

10from pathlib import Path 

11from typing import Optional 

12from rich.console import Console 

13from rich.markdown import Markdown 

14from rich.panel import Panel 

15from rich.live import Live 

16from rich.spinner import Spinner 

17 

18from .chat_engine import ChatEngine, StreamingState, ToolCallStatus 

19from .tools_registry import tools_registry 

20from .config import settings 

21from .client import get_async_client 

22 

23console = Console() 

24 

25 

26class OsirisCLI: 

27 """ 

28 Main CLI interface - rebuilt to match Gemini/Letta patterns. 

29  

30 Key improvements: 

31 - Autonomous tool execution (no round limits) 

32 - Proper state machine 

33 - Clean event-driven architecture 

34 """ 

35 

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

37 self.agent_name = agent_name 

38 self.running = True 

39 

40 # Initialize chat engine 

41 self.chat_engine = ChatEngine( 

42 client=get_async_client(), 

43 settings=settings, 

44 tools_registry=tools_registry, 

45 max_tool_calls_per_turn=50, # Safety limit 

46 enable_auto_approval=settings.yolo_mode 

47 ) 

48 

49 # Load agent memory/context 

50 self.system_context = self._load_system_context() 

51 

52 def _load_system_context(self) -> str: 

53 """Load system context from OSIRIS.md and agent memory""" 

54 context_parts = [] 

55 

56 # Add base system prompt 

57 if settings.system_prompt: 

58 context_parts.append(settings.system_prompt) 

59 

60 # Load OSIRIS.md if it exists 

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

62 if osiris_md.exists(): 

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

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

65 

66 # Load agent memory 

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

68 if memory_file.exists(): 

69 import json 

70 try: 

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

72 memory = json.load(f) 

73 if memory.get("core_memory"): 

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

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

76 except: 

77 pass 

78 

79 return "\n\n".join(context_parts) 

80 

81 def show_welcome(self): 

82 """Display welcome message""" 

83 welcome = f""" 

84# Osiris CLI v5.0 

85 

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

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

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

89 

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

91 """ 

92 console.print(Markdown(welcome)) 

93 console.print() 

94 

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

96 """ 

97 Handle a user message with autonomous tool execution. 

98 This is where the magic happens - no more round limits! 

99 """ 

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

101 

102 current_tool_call = None 

103 tool_count = 0 

104 

105 try: 

106 # Stream response with autonomous tool execution 

107 async for event in self.chat_engine.send_message( 

108 user_input, 

109 system_context=self.system_context 

110 ): 

111 event_type = event["type"] 

112 

113 if event_type == "content": 

114 # Stream text content 

115 console.print(event["text"], end="") 

116 

117 elif event_type == "tool_call": 

118 # Tool is being called 

119 tool = event["tool"] 

120 tool_count += 1 

121 

122 console.print(f"\n\n[cyan]→ Tool Call #{tool_count}:[/cyan] {tool.name}") 

123 if tool.arguments: 

124 args_str = ", ".join(f"{k}={v}" for k, v in tool.arguments.items()) 

125 console.print(f" [dim]{args_str[:100]}{'...' if len(args_str) > 100 else ''}[/dim]") 

126 

127 current_tool_call = tool 

128 

129 elif event_type == "tool_result": 

130 # Tool execution completed 

131 tool = event["tool"] 

132 

133 if tool.status == ToolCallStatus.SUCCESS: 

134 result_preview = (tool.result or "")[:150] 

135 console.print(f" [green]✓[/green] [dim]{result_preview}{'...' if len(tool.result or '') > 150 else ''}[/dim]\n") 

136 else: 

137 console.print(f" [red]✗ Error:[/red] {tool.error}\n") 

138 

139 elif event_type == "state_change": 

140 # State changed (for debugging/monitoring) 

141 pass 

142 

143 elif event_type == "error": 

144 # Error occurred 

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

146 

147 console.print("\n") 

148 

149 except KeyboardInterrupt: 

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

151 except Exception as e: 

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

153 

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

155 """Handle slash commands. Returns True if command was handled.""" 

156 parts = command.split(maxsplit=1) 

157 cmd = parts[0].lower() 

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

159 

160 if cmd == "/help": 

161 self.show_help() 

162 return True 

163 

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

165 self.running = False 

166 return True 

167 

168 elif cmd == "/clear": 

169 import os 

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

171 return True 

172 

173 elif cmd == "/reset": 

174 self.chat_engine.reset() 

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

176 return True 

177 

178 elif cmd == "/yolo": 

179 settings.yolo_mode = not settings.yolo_mode 

180 settings.save() 

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

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

183 return True 

184 

185 elif cmd == "/model": 

186 # Model selection 

187 from .live_menus import select_model 

188 selected = await select_model() 

189 if selected: 

190 get_async_client().reinitialize() 

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

192 return True 

193 

194 elif cmd == "/provider": 

195 # Provider selection 

196 from .live_menus import select_provider 

197 selected = await select_provider() 

198 if selected: 

199 get_async_client().reinitialize() 

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

201 return True 

202 

203 elif cmd == "/history": 

204 # Show conversation history 

205 conversation = self.chat_engine.get_conversation() 

206 if not conversation: 

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

208 else: 

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

210 for msg in conversation[-10:]: # Show last 10 

211 role = msg["role"] 

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

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

214 console.print() 

215 return True 

216 

217 elif cmd == "/tools": 

218 # List available tools 

219 tools = tools_registry.list_tools() 

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

221 for tool in tools: 

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

223 console.print() 

224 return True 

225 

226 return False 

227 

228 def show_help(self): 

229 """Show help message""" 

230 help_text = """ 

231# Osiris CLI v5.0 - Command Reference 

232 

233## Core Commands 

234- `/help` - Show this help 

235- `/exit` - Exit Osiris 

236- `/clear` - Clear screen 

237- `/reset` - Reset conversation 

238 

239## Configuration 

240- `/model` - Change AI model 

241- `/provider` - Change provider 

242- `/yolo` - Toggle autonomous mode 

243 

244## Information 

245- `/history` - Show conversation history 

246- `/tools` - List available tools 

247 

248## Features 

249- **Autonomous Tool Execution**: No round limits, tools execute until completion 

250- **Smart Context**: Automatically loads OSIRIS.md and agent memory 

251- **Safety Limits**: Max 50 tool calls per turn to prevent infinite loops 

252 """ 

253 console.print(Markdown(help_text)) 

254 console.print() 

255 

256 async def run(self): 

257 """Main CLI loop""" 

258 self.show_welcome() 

259 

260 while self.running: 

261 try: 

262 # Get user input 

263 user_input = await asyncio.to_thread( 

264 input, 

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

266 ) 

267 

268 if not user_input.strip(): 

269 continue 

270 

271 # Handle slash commands 

272 if user_input.startswith("/"): 

273 if await self.handle_slash_command(user_input): 

274 continue 

275 

276 # Handle regular message 

277 await self.handle_message(user_input) 

278 

279 except KeyboardInterrupt: 

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

281 continue 

282 except EOFError: 

283 break 

284 

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

286 

287 

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

289 """Entry point for the CLI""" 

290 cli = OsirisCLI(agent_name=agent_name) 

291 

292 if prompt: 

293 # Non-interactive mode 

294 await cli.handle_message(prompt) 

295 else: 

296 # Interactive mode 

297 await cli.run()