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

122 statements  

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

1#!/usr/bin/env python3 

2""" 

3Agent Selector - Letta-style agent management 

4Allows users to select, create, and switch between agents 

5""" 

6 

7import json 

8from pathlib import Path 

9from datetime import datetime 

10from typing import Optional, List, Dict 

11from rich.console import Console 

12from rich.table import Table 

13from rich.panel import Panel 

14from rich.prompt import Prompt, Confirm 

15from rich.text import Text 

16 

17console = Console() 

18 

19 

20class AgentSelector: 

21 """ 

22 Interactive agent selector (Letta-style) 

23 """ 

24 

25 COLORS = { 

26 "primary": "#00D9FF", 

27 "success": "#00FF9F", 

28 "warning": "#FFB800", 

29 "muted": "#6B7280", 

30 "accent": "#A78BFA", 

31 } 

32 

33 def __init__(self): 

34 self.agents_dir = Path.home() / ".osiris" / "agents" 

35 self.agents_dir.mkdir(parents=True, exist_ok=True) 

36 

37 def list_agents(self) -> List[Dict]: 

38 """List all available agents""" 

39 agents = [] 

40 

41 for agent_file in self.agents_dir.glob("*.json"): 

42 try: 

43 with open(agent_file, 'r') as f: 

44 data = json.load(f) 

45 

46 # Calculate last used 

47 last_used = data.get("last_used", "never") 

48 last_used_dt = None 

49 if last_used != "never": 

50 try: 

51 dt = datetime.fromisoformat(last_used) 

52 last_used_dt = dt 

53 now = datetime.now() 

54 delta = now - dt 

55 

56 if delta.seconds < 60: 

57 last_used = "just now" 

58 elif delta.seconds < 3600: 

59 last_used = f"{delta.seconds // 60}min ago" 

60 elif delta.days == 0: 

61 last_used = f"{delta.seconds // 3600}h ago" 

62 elif delta.days == 1: 

63 last_used = "yesterday" 

64 else: 

65 last_used = f"{delta.days}d ago" 

66 except: 

67 pass 

68 

69 agents.append({ 

70 "name": agent_file.stem, 

71 "persona": data.get("persona", "General assistant")[:40], 

72 "skills": data.get("skills", []), 

73 "facts": len(data.get("core_memory", {})), 

74 "last_used": last_used, 

75 "last_used_dt": last_used_dt, 

76 "file": agent_file 

77 }) 

78 except: 

79 continue 

80 

81 # Sort by last used (most recent first) 

82 agents.sort(key=lambda x: x.get("last_used_dt") or datetime.min, reverse=True) 

83 return agents 

84 

85 def select_agent(self) -> Optional[str]: 

86 """Interactive agent selector""" 

87 agents = self.list_agents() 

88 

89 console.clear() 

90 console.print() 

91 

92 # Title 

93 title = Text() 

94 title.append("Select Agent", style=f"bold {self.COLORS['primary']}") 

95 console.print(title, justify="center") 

96 console.print() 

97 

98 # Create table 

99 table = Table(show_header=True, header_style=f"bold {self.COLORS['accent']}") 

100 table.add_column("#", style="dim", width=3) 

101 table.add_column("Name", style=f"bold {self.COLORS['primary']}") 

102 table.add_column("Persona", style=self.COLORS["muted"]) 

103 table.add_column("Skills", style=self.COLORS["success"]) 

104 table.add_column("Facts", style=self.COLORS["warning"], justify="right") 

105 table.add_column("Last Used", style="dim") 

106 

107 for i, agent in enumerate(agents, 1): 

108 skills_str = ", ".join(agent["skills"][:2]) 

109 if len(agent["skills"]) > 2: 

110 skills_str += f" +{len(agent['skills']) - 2}" 

111 

112 table.add_row( 

113 str(i), 

114 agent["name"], 

115 agent["persona"], 

116 skills_str or "none", 

117 str(agent["facts"]), 

118 agent["last_used"] 

119 ) 

120 

121 # Add "create new" option 

122 table.add_row( 

123 str(len(agents) + 1), 

124 Text("+ Create new agent", style=f"bold {self.COLORS['success']}"), 

125 "", 

126 "", 

127 "", 

128 "" 

129 ) 

130 

131 console.print(table) 

132 console.print() 

133 

134 # Get selection 

135 try: 

136 choice = Prompt.ask( 

137 f"[{self.COLORS['primary']}]Choose agent[/{self.COLORS['primary']}]", 

138 default="1" if agents else str(len(agents) + 1) 

139 ) 

140 

141 if choice.isdigit(): 

142 idx = int(choice) - 1 

143 if 0 <= idx < len(agents): 

144 selected = agents[idx]["name"] 

145 console.print(f"\n[{self.COLORS['success']}]✓ Selected: {selected}[/{self.COLORS['success']}]\n") 

146 return selected 

147 elif idx == len(agents): 

148 return self.create_agent() 

149 

150 console.print(f"\n[{self.COLORS['warning']}]Invalid choice. Using 'main'[/{self.COLORS['warning']}]\n") 

151 return "main" 

152 

153 except (ValueError, KeyboardInterrupt): 

154 console.print(f"\n[{self.COLORS['warning']}]Cancelled. Using 'main'[/{self.COLORS['warning']}]\n") 

155 return "main" 

156 

157 def create_agent(self) -> Optional[str]: 

158 """Create a new agent interactively""" 

159 console.print() 

160 console.print(f"[bold {self.COLORS['primary']}]Create New Agent[/bold {self.COLORS['primary']}]\n") 

161 

162 try: 

163 # Get agent name 

164 name = Prompt.ask( 

165 f"[{self.COLORS['primary']}]Agent name[/{self.COLORS['primary']}]", 

166 default="assistant" 

167 ) 

168 

169 # Get persona 

170 console.print(f"\n[{self.COLORS['accent']}]Choose a persona:[/{self.COLORS['accent']}]") 

171 

172 personas = { 

173 "1": ("General Assistant", "A helpful general-purpose AI assistant"), 

174 "2": ("Code Expert", "Senior software engineer specializing in code"), 

175 "3": ("Code Reviewer", "Focused on code quality and best practices"), 

176 "4": ("Debugger", "Expert at finding and fixing bugs"), 

177 "5": ("DevOps Engineer", "Infrastructure and deployment specialist"), 

178 "6": ("Data Analyst", "Data science and analytics expert"), 

179 "7": ("Technical Writer", "Documentation specialist"), 

180 "8": ("System Architect", "Software architecture and design expert"), 

181 "9": ("Custom", "Define your own persona") 

182 } 

183 

184 for key, (title, desc) in personas.items(): 

185 console.print(f" {key}. [{self.COLORS['primary']}]{title}[/{self.COLORS['primary']}] - [{self.COLORS['muted']}]{desc}[/{self.COLORS['muted']}]") 

186 

187 persona_choice = Prompt.ask( 

188 f"\n[{self.COLORS['primary']}]Choose persona[/{self.COLORS['primary']}]", 

189 default="1" 

190 ) 

191 

192 if persona_choice == "9": 

193 persona = Prompt.ask(f"[{self.COLORS['primary']}]Enter custom persona[/{self.COLORS['primary']}]") 

194 elif persona_choice in personas: 

195 persona = personas[persona_choice][1] 

196 else: 

197 persona = personas["1"][1] 

198 

199 # Get skills 

200 console.print(f"\n[{self.COLORS['muted']}]Add skills (comma-separated, or press Enter to skip):[/{self.COLORS['muted']}]") 

201 skills_input = Prompt.ask( 

202 f"[{self.COLORS['primary']}]Skills[/{self.COLORS['primary']}]", 

203 default="" 

204 ) 

205 skills = [s.strip() for s in skills_input.split(",") if s.strip()] 

206 

207 # Create agent data 

208 agent_data = { 

209 "name": name, 

210 "persona": persona, 

211 "skills": skills, 

212 "core_memory": {}, 

213 "created": datetime.now().isoformat(), 

214 "last_used": datetime.now().isoformat() 

215 } 

216 

217 # Save agent 

218 agent_file = self.agents_dir / f"{name}.json" 

219 with open(agent_file, 'w') as f: 

220 json.dump(agent_data, f, indent=2) 

221 

222 console.print(f"\n[{self.COLORS['success']}]✓ Created agent: {name}[/{self.COLORS['success']}]\n") 

223 return name 

224 

225 except KeyboardInterrupt: 

226 console.print(f"\n[{self.COLORS['warning']}]Cancelled[/{self.COLORS['warning']}]\n") 

227 return "main" 

228 

229 def update_last_used(self, agent_name: str): 

230 """Update agent's last used timestamp""" 

231 agent_file = self.agents_dir / f"{agent_name}.json" 

232 if agent_file.exists(): 

233 try: 

234 with open(agent_file, 'r') as f: 

235 data = json.load(f) 

236 

237 data["last_used"] = datetime.now().isoformat() 

238 

239 with open(agent_file, 'w') as f: 

240 json.dump(data, f, indent=2) 

241 except: 

242 pass 

243 

244 

245# Global instance 

246agent_selector = AgentSelector()