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
« 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.
6Complete rewrite of the chat/agent loop to match proven agentic patterns.
7"""
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
18from .chat_engine import ChatEngine, StreamingState, ToolCallStatus
19from .tools_registry import tools_registry
20from .config import settings
21from .client import get_async_client
23console = Console()
26class OsirisCLI:
27 """
28 Main CLI interface - rebuilt to match Gemini/Letta patterns.
30 Key improvements:
31 - Autonomous tool execution (no round limits)
32 - Proper state machine
33 - Clean event-driven architecture
34 """
36 def __init__(self, agent_name: str = "main"):
37 self.agent_name = agent_name
38 self.running = True
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 )
49 # Load agent memory/context
50 self.system_context = self._load_system_context()
52 def _load_system_context(self) -> str:
53 """Load system context from OSIRIS.md and agent memory"""
54 context_parts = []
56 # Add base system prompt
57 if settings.system_prompt:
58 context_parts.append(settings.system_prompt)
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()}")
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
79 return "\n\n".join(context_parts)
81 def show_welcome(self):
82 """Display welcome message"""
83 welcome = f"""
84# Osiris CLI v5.0
86**Agent**: {self.agent_name}
87**Model**: {settings.default_model}
88**Mode**: {'YOLO (autonomous)' if settings.yolo_mode else 'Interactive'}
90Type `/help` for commands or start chatting.
91 """
92 console.print(Markdown(welcome))
93 console.print()
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")
102 current_tool_call = None
103 tool_count = 0
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"]
113 if event_type == "content":
114 # Stream text content
115 console.print(event["text"], end="")
117 elif event_type == "tool_call":
118 # Tool is being called
119 tool = event["tool"]
120 tool_count += 1
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]")
127 current_tool_call = tool
129 elif event_type == "tool_result":
130 # Tool execution completed
131 tool = event["tool"]
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")
139 elif event_type == "state_change":
140 # State changed (for debugging/monitoring)
141 pass
143 elif event_type == "error":
144 # Error occurred
145 console.print(f"\n[red]Error:[/red] {event['message']}")
147 console.print("\n")
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")
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 ""
160 if cmd == "/help":
161 self.show_help()
162 return True
164 elif cmd in ["/exit", "/quit"]:
165 self.running = False
166 return True
168 elif cmd == "/clear":
169 import os
170 os.system('clear' if os.name != 'nt' else 'cls')
171 return True
173 elif cmd == "/reset":
174 self.chat_engine.reset()
175 console.print("[green]✓[/green] Conversation reset\n")
176 return True
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
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
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
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
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
226 return False
228 def show_help(self):
229 """Show help message"""
230 help_text = """
231# Osiris CLI v5.0 - Command Reference
233## Core Commands
234- `/help` - Show this help
235- `/exit` - Exit Osiris
236- `/clear` - Clear screen
237- `/reset` - Reset conversation
239## Configuration
240- `/model` - Change AI model
241- `/provider` - Change provider
242- `/yolo` - Toggle autonomous mode
244## Information
245- `/history` - Show conversation history
246- `/tools` - List available tools
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()
256 async def run(self):
257 """Main CLI loop"""
258 self.show_welcome()
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 )
268 if not user_input.strip():
269 continue
271 # Handle slash commands
272 if user_input.startswith("/"):
273 if await self.handle_slash_command(user_input):
274 continue
276 # Handle regular message
277 await self.handle_message(user_input)
279 except KeyboardInterrupt:
280 console.print("\n[yellow]Use /exit to quit[/yellow]\n")
281 continue
282 except EOFError:
283 break
285 console.print("\n[cyan]Goodbye![/cyan]\n")
288async def main(agent_name: str = "main", prompt: Optional[str] = None):
289 """Entry point for the CLI"""
290 cli = OsirisCLI(agent_name=agent_name)
292 if prompt:
293 # Non-interactive mode
294 await cli.handle_message(prompt)
295 else:
296 # Interactive mode
297 await cli.run()