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
« 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"""
7import asyncio
8import time
9from pathlib import Path
10from typing import Optional
11from rich.console import Console
12from rich.markdown import Markdown
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)
27console = Console()
30class OsirisCLI_Enhanced:
31 """
32 Enhanced CLI with Gemini + Letta UX patterns
33 """
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 = []
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 )
52 # Load agent memory/context
53 self.system_context, self.memory = self._load_system_context()
55 # Display system
56 self.stream_display = StreamingDisplay()
57 self.tool_display = ToolDisplay()
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 = {}
64 # Add base system prompt
65 if settings.system_prompt:
66 context_parts.append(settings.system_prompt)
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()}")
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
87 return "\n\n".join(context_parts), memory
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()
96 # Welcome message
97 welcome = f"""
98# Osiris CLI v5.1
100**Agent**: {self.agent_name}
101**Model**: {settings.default_model}
102**Mode**: {'YOLO (autonomous)' if settings.yolo_mode else 'Interactive'}
104Type `/help` for commands or start chatting.
105 """
106 console.print(Markdown(welcome))
107 console.print()
109 async def handle_message(self, user_input: str):
110 """Handle message with enhanced display"""
111 console.print("\n[bold]Response:[/bold]\n")
113 tool_count = 0
114 response_text = ""
115 tool_start_time = None
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"]
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)
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
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()
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
145 if tool.status == ToolCallStatus.SUCCESS:
146 self.tool_display.show_tool_result(
147 tool.result or "",
148 success=True,
149 duration=duration
150 )
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 )
163 console.print() # Newline after tool
165 elif event_type == "error":
166 console.print(f"\n[red]Error:[/red] {event['message']}")
168 console.print("\n")
170 # Track key action
171 if tool_count > 0:
172 self.key_actions.append(f"Used {tool_count} tools to: {user_input[:50]}...")
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")
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 ""
185 if cmd == "/help":
186 self.show_help()
187 return True
189 elif cmd in ["/exit", "/quit"]:
190 self.show_session_summary()
191 self.running = False
192 return True
194 elif cmd == "/clear":
195 import os
196 os.system('clear' if os.name != 'nt' else 'cls')
197 return True
199 elif cmd == "/reset":
200 self.chat_engine.reset()
201 console.print("[green]✓[/green] Conversation reset\n")
202 return True
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
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
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
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
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
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
256 elif cmd == "/summary":
257 self.show_session_summary()
258 return True
260 return False
262 def show_help(self):
263 """Show help"""
264 help_text = """
265# Osiris CLI v5.1 - Command Reference
267## Core Commands
268- `/help` - Show this help
269- `/exit` - Exit Osiris (shows session summary)
270- `/clear` - Clear screen
271- `/reset` - Reset conversation
273## Configuration
274- `/model` - Change AI model
275- `/provider` - Change provider
276- `/yolo` - Toggle autonomous mode
278## Information
279- `/history` - Show conversation history
280- `/tools` - List available tools
281- `/memory` - Show agent memory
282- `/summary` - Show session summary
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()
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"
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 )
306 async def run(self):
307 """Main CLI loop"""
308 self.show_welcome()
310 while self.running:
311 try:
312 user_input = await asyncio.to_thread(
313 input,
314 "\n[bold cyan]You:[/bold cyan] "
315 )
317 if not user_input.strip():
318 continue
320 if user_input.startswith("/"):
321 if await self.handle_slash_command(user_input):
322 continue
324 await self.handle_message(user_input)
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
333 console.print("\n[cyan]Goodbye![/cyan]\n")
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)
340 if prompt:
341 await cli.handle_message(prompt)
342 cli.show_session_summary()
343 else:
344 await cli.run()