hanzo 0.3.21__py3-none-any.whl → 0.3.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo might be problematic. Click here for more details.

hanzo/dev.py CHANGED
@@ -661,10 +661,11 @@ class HanzoDevREPL:
661
661
  "help": self.cmd_help,
662
662
  "exit": self.cmd_exit,
663
663
  }
664
-
664
+
665
665
  # Initialize memory manager
666
666
  from .memory_manager import MemoryManager
667
- workspace = getattr(orchestrator, 'workspace_dir', '/tmp/hanzo')
667
+
668
+ workspace = getattr(orchestrator, "workspace_dir", "/tmp/hanzo")
668
669
  self.memory_manager = MemoryManager(workspace)
669
670
 
670
671
  async def run(self):
@@ -676,55 +677,64 @@ class HanzoDevREPL:
676
677
  from rich.console import Group
677
678
  from prompt_toolkit import prompt
678
679
  from prompt_toolkit.styles import Style
679
-
680
+
680
681
  # Define Claude-like style for prompt_toolkit
681
- claude_style = Style.from_dict({
682
- '': '#333333', # Default text color
683
- 'prompt': '#666666', # Gray prompt arrow
684
- })
685
-
682
+ claude_style = Style.from_dict(
683
+ {
684
+ "": "#333333", # Default text color
685
+ "prompt": "#666666", # Gray prompt arrow
686
+ }
687
+ )
688
+
686
689
  # Use a predefined box style that's similar to Claude
687
690
  from rich.box import ROUNDED
691
+
688
692
  LIGHT_GRAY_BOX = ROUNDED
689
-
693
+
690
694
  # Header
691
695
  console.print()
692
- console.print(Panel(
693
- "[bold cyan]Hanzo Dev - AI Chat[/bold cyan]\n"
694
- "[dim]Chat naturally or use /commands • Type /help for available commands[/dim]",
695
- box=LIGHT_GRAY_BOX,
696
- style="dim white",
697
- padding=(0, 1)
698
- ))
696
+ console.print(
697
+ Panel(
698
+ "[bold cyan]Hanzo Dev - AI Chat[/bold cyan]\n"
699
+ "[dim]Chat naturally or use /commands • Type /help for available commands[/dim]",
700
+ box=LIGHT_GRAY_BOX,
701
+ style="dim white",
702
+ padding=(0, 1),
703
+ )
704
+ )
699
705
  console.print()
700
706
 
707
+ # Check for available API keys and show status
708
+ from .fallback_handler import FallbackHandler
709
+
710
+ handler = FallbackHandler()
711
+ if not handler.fallback_order:
712
+ console.print("[yellow]⚠️ No API keys detected[/yellow]")
713
+ console.print(
714
+ "[dim]Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI[/dim]"
715
+ )
716
+ console.print()
717
+ else:
718
+ primary = handler.fallback_order[0][1]
719
+ console.print(f"[green]✅ Using {primary} for AI responses[/green]")
720
+ console.print()
721
+
701
722
  while True:
702
723
  try:
703
- # Draw input box border (top)
704
- console.print("[dim white]╭" + "─" * 78 + "╮[/dim white]")
705
-
706
- # Get input with styled prompt inside the box
707
- console.print("[dim white]│[/dim white] ", end="")
708
-
724
+ # Simple prompt without box borders to avoid rendering issues
709
725
  try:
710
- # Get input - using simple input() wrapped in executor for async
711
- # The visual box is drawn by console.print statements
726
+ # Add spacing to prevent UI cutoff at bottom
712
727
  user_input = await asyncio.get_event_loop().run_in_executor(
713
728
  None,
714
729
  input,
715
- '' # Using › instead of > for a more modern look
730
+ "", # Clean prompt
716
731
  )
717
-
718
- # Draw input box border (bottom)
719
- console.print("[dim white]╰" + "─" * 78 + "╯[/dim white]")
720
-
732
+ console.print() # Add spacing after input
733
+
721
734
  except EOFError:
722
735
  console.print() # New line before exit
723
- console.print("[dim white]╰" + "─" * 78 + "╯[/dim white]")
724
736
  break
725
737
  except KeyboardInterrupt:
726
- console.print() # Complete the box
727
- console.print("[dim white]╰" + "─" * 78 + "╯[/dim white]")
728
738
  console.print("\n[dim yellow]Use /exit to quit[/dim]")
729
739
  continue
730
740
 
@@ -737,20 +747,25 @@ class HanzoDevREPL:
737
747
  parts = user_input[1:].strip().split(maxsplit=1)
738
748
  cmd = parts[0].lower()
739
749
  args = parts[1] if len(parts) > 1 else ""
740
-
750
+
741
751
  if cmd in self.commands:
742
752
  await self.commands[cmd](args)
743
753
  else:
744
754
  console.print(f"[yellow]Unknown command: /{cmd}[/yellow]")
745
755
  console.print("Type /help for available commands")
746
-
756
+
747
757
  elif user_input.startswith("#"):
748
758
  # Handle memory/context commands
749
759
  from .memory_manager import handle_memory_command
750
- handled = handle_memory_command(user_input, self.memory_manager, console)
760
+
761
+ handled = handle_memory_command(
762
+ user_input, self.memory_manager, console
763
+ )
751
764
  if not handled:
752
- console.print("[yellow]Unknown memory command. Use #memory help[/yellow]")
753
-
765
+ console.print(
766
+ "[yellow]Unknown memory command. Use #memory help[/yellow]"
767
+ )
768
+
754
769
  else:
755
770
  # Natural chat - send directly to AI agents
756
771
  await self.chat_with_agents(user_input)
@@ -910,132 +925,183 @@ Examples:
910
925
  self.orchestrator.shutdown()
911
926
  console.print("[green]Goodbye![/green]")
912
927
  sys.exit(0)
913
-
928
+
914
929
  async def chat_with_agents(self, message: str):
915
930
  """Send message to AI agents for natural chat."""
916
931
  try:
917
932
  # Add message to memory
918
933
  self.memory_manager.add_message("user", message)
919
-
934
+
920
935
  # Get memory context
921
936
  memory_context = self.memory_manager.summarize_for_ai()
922
-
937
+
923
938
  # Enhance message with context
924
939
  if memory_context:
925
940
  enhanced_message = f"{memory_context}\n\nUser: {message}"
926
941
  else:
927
942
  enhanced_message = message
928
-
943
+
929
944
  # Try smart fallback if no specific model configured
930
- if not hasattr(self.orchestrator, 'orchestrator_model') or \
931
- self.orchestrator.orchestrator_model == "auto":
945
+ if (
946
+ not hasattr(self.orchestrator, "orchestrator_model")
947
+ or self.orchestrator.orchestrator_model == "auto"
948
+ ):
932
949
  # Use streaming if available
933
950
  from .streaming import stream_with_fallback
951
+
934
952
  response = await stream_with_fallback(enhanced_message, console)
935
-
953
+
936
954
  if response:
937
955
  # Save AI response to memory
938
956
  self.memory_manager.add_message("assistant", response)
939
957
  # Response already displayed by streaming handler
940
958
  return
941
959
  else:
942
- console.print("[red]No AI options available. Please configure API keys or install tools.[/red]")
960
+ console.print(
961
+ "[red]No AI options available. Please configure API keys or install tools.[/red]"
962
+ )
943
963
  return
944
-
964
+
945
965
  # For codex and other CLI tools, go straight to direct API chat
946
- if hasattr(self.orchestrator, 'orchestrator_model'):
966
+ if hasattr(self.orchestrator, "orchestrator_model"):
947
967
  model = self.orchestrator.orchestrator_model
948
- if model in ["codex", "openai-cli", "openai-codex", "claude", "claude-code",
949
- "claude-desktop", "gemini", "gemini-cli", "google-gemini",
950
- "hanzo-ide", "hanzo-dev-ide", "ide", "codestral", "codestral-free",
951
- "free", "mistral-free", "starcoder", "starcoder2", "free-starcoder"] or \
952
- model.startswith("local:"):
968
+ if model in [
969
+ "codex",
970
+ "openai-cli",
971
+ "openai-codex",
972
+ "claude",
973
+ "claude-code",
974
+ "claude-desktop",
975
+ "gemini",
976
+ "gemini-cli",
977
+ "google-gemini",
978
+ "hanzo-ide",
979
+ "hanzo-dev-ide",
980
+ "ide",
981
+ "codestral",
982
+ "codestral-free",
983
+ "free",
984
+ "mistral-free",
985
+ "starcoder",
986
+ "starcoder2",
987
+ "free-starcoder",
988
+ ] or model.startswith("local:"):
953
989
  # Use direct API/CLI chat for these models
954
990
  await self._direct_api_chat(message)
955
991
  return
956
-
992
+
957
993
  # Show thinking indicator for network orchestrators
958
994
  console.print("[dim]Thinking...[/dim]")
959
-
995
+
960
996
  # Check if we have a network orchestrator with actual AI
961
- if hasattr(self.orchestrator, 'execute_with_network'):
997
+ if hasattr(self.orchestrator, "execute_with_network"):
962
998
  # Use the network orchestrator (GPT-4, GPT-5, etc.)
963
999
  result = await self.orchestrator.execute_with_network(
964
- task=message,
965
- context={"mode": "chat", "interactive": True}
1000
+ task=message, context={"mode": "chat", "interactive": True}
966
1001
  )
967
-
1002
+
968
1003
  if result.get("output"):
969
1004
  # Display AI response in a styled panel
970
1005
  console.print()
971
1006
  from rich.panel import Panel
972
- console.print(Panel(
973
- result['output'],
974
- title="[bold cyan]AI Response[/bold cyan]",
975
- title_align="left",
976
- border_style="dim cyan",
977
- padding=(1, 2)
978
- ))
1007
+
1008
+ console.print(
1009
+ Panel(
1010
+ result["output"],
1011
+ title="[bold cyan]AI Response[/bold cyan]",
1012
+ title_align="left",
1013
+ border_style="dim cyan",
1014
+ padding=(1, 2),
1015
+ )
1016
+ )
979
1017
  elif result.get("error"):
980
1018
  console.print(f"\n[red]Error:[/red] {result['error']}")
981
1019
  else:
982
1020
  console.print("\n[yellow]No response from agent[/yellow]")
983
-
984
- elif hasattr(self.orchestrator, 'execute_with_critique'):
1021
+
1022
+ elif hasattr(self.orchestrator, "execute_with_critique"):
985
1023
  # Use multi-Claude orchestrator - but now it will use real AI!
986
1024
  result = await self.orchestrator.execute_with_critique(message)
987
-
1025
+
988
1026
  if result.get("output"):
989
1027
  # Display AI response in a styled panel
990
1028
  console.print()
991
1029
  from rich.panel import Panel
992
- console.print(Panel(
993
- result['output'],
994
- title="[bold cyan]AI Response[/bold cyan]",
995
- title_align="left",
996
- border_style="dim cyan",
997
- padding=(1, 2)
998
- ))
1030
+
1031
+ console.print(
1032
+ Panel(
1033
+ result["output"],
1034
+ title="[bold cyan]AI Response[/bold cyan]",
1035
+ title_align="left",
1036
+ border_style="dim cyan",
1037
+ padding=(1, 2),
1038
+ )
1039
+ )
999
1040
  else:
1000
1041
  console.print("\n[yellow]No response from agent[/yellow]")
1001
-
1042
+
1002
1043
  else:
1003
1044
  # Fallback to direct API call if available
1004
1045
  await self._direct_api_chat(message)
1005
-
1046
+
1006
1047
  except Exception as e:
1007
1048
  console.print(f"[red]Error connecting to AI: {e}[/red]")
1008
1049
  console.print("[yellow]Make sure you have API keys configured:[/yellow]")
1009
1050
  console.print(" • OPENAI_API_KEY for GPT models")
1010
1051
  console.print(" • ANTHROPIC_API_KEY for Claude")
1011
1052
  console.print(" • Or use --orchestrator local:llama3.2 for local models")
1012
-
1053
+
1013
1054
  async def _direct_api_chat(self, message: str):
1014
1055
  """Direct API chat fallback when network orchestrator isn't available."""
1015
1056
  import os
1016
-
1057
+
1017
1058
  # Check for CLI tools and free/local options first
1018
- if self.orchestrator.orchestrator_model in ["codex", "openai-cli", "openai-codex"]:
1059
+ if self.orchestrator.orchestrator_model in [
1060
+ "codex",
1061
+ "openai-cli",
1062
+ "openai-codex",
1063
+ ]:
1019
1064
  # Use OpenAI CLI (Codex)
1020
1065
  await self._use_openai_cli(message)
1021
1066
  return
1022
- elif self.orchestrator.orchestrator_model in ["claude", "claude-code", "claude-desktop"]:
1067
+ elif self.orchestrator.orchestrator_model in [
1068
+ "claude",
1069
+ "claude-code",
1070
+ "claude-desktop",
1071
+ ]:
1023
1072
  # Use Claude Desktop/Code
1024
1073
  await self._use_claude_cli(message)
1025
1074
  return
1026
- elif self.orchestrator.orchestrator_model in ["gemini", "gemini-cli", "google-gemini"]:
1075
+ elif self.orchestrator.orchestrator_model in [
1076
+ "gemini",
1077
+ "gemini-cli",
1078
+ "google-gemini",
1079
+ ]:
1027
1080
  # Use Gemini CLI
1028
1081
  await self._use_gemini_cli(message)
1029
1082
  return
1030
- elif self.orchestrator.orchestrator_model in ["hanzo-ide", "hanzo-dev-ide", "ide"]:
1083
+ elif self.orchestrator.orchestrator_model in [
1084
+ "hanzo-ide",
1085
+ "hanzo-dev-ide",
1086
+ "ide",
1087
+ ]:
1031
1088
  # Use Hanzo Dev IDE from ~/work/hanzo/ide
1032
1089
  await self._use_hanzo_ide(message)
1033
1090
  return
1034
- elif self.orchestrator.orchestrator_model in ["codestral", "codestral-free", "free", "mistral-free"]:
1091
+ elif self.orchestrator.orchestrator_model in [
1092
+ "codestral",
1093
+ "codestral-free",
1094
+ "free",
1095
+ "mistral-free",
1096
+ ]:
1035
1097
  # Use free Mistral Codestral API
1036
1098
  await self._use_free_codestral(message)
1037
1099
  return
1038
- elif self.orchestrator.orchestrator_model in ["starcoder", "starcoder2", "free-starcoder"]:
1100
+ elif self.orchestrator.orchestrator_model in [
1101
+ "starcoder",
1102
+ "starcoder2",
1103
+ "free-starcoder",
1104
+ ]:
1039
1105
  # Use free StarCoder via HuggingFace
1040
1106
  await self._use_free_starcoder(message)
1041
1107
  return
@@ -1043,75 +1109,117 @@ Examples:
1043
1109
  # Use local model via Ollama or LM Studio
1044
1110
  await self._use_local_model(message)
1045
1111
  return
1046
-
1047
- # Try OpenAI first
1048
- if os.getenv("OPENAI_API_KEY"):
1112
+
1113
+ # Use the fallback handler to intelligently try available options
1114
+ from .fallback_handler import smart_chat
1115
+
1116
+ response = await smart_chat(message, console=console)
1117
+
1118
+ if response:
1119
+ from rich.panel import Panel
1120
+
1121
+ console.print()
1122
+ console.print(
1123
+ Panel(
1124
+ response,
1125
+ title="[bold cyan]AI Response[/bold cyan]",
1126
+ title_align="left",
1127
+ border_style="dim cyan",
1128
+ padding=(1, 2),
1129
+ )
1130
+ )
1131
+ return
1132
+
1133
+ # Try OpenAI first explicitly (in case fallback handler missed it)
1134
+ openai_key = os.environ.get("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY")
1135
+ if openai_key:
1049
1136
  try:
1050
1137
  from openai import AsyncOpenAI
1051
-
1138
+
1052
1139
  client = AsyncOpenAI()
1053
1140
  response = await client.chat.completions.create(
1054
1141
  model=self.orchestrator.orchestrator_model or "gpt-4",
1055
1142
  messages=[
1056
- {"role": "system", "content": "You are a helpful AI coding assistant."},
1057
- {"role": "user", "content": message}
1143
+ {
1144
+ "role": "system",
1145
+ "content": "You are a helpful AI coding assistant.",
1146
+ },
1147
+ {"role": "user", "content": message},
1058
1148
  ],
1059
1149
  temperature=0.7,
1060
- max_tokens=2000
1150
+ max_tokens=2000,
1061
1151
  )
1062
-
1152
+
1063
1153
  if response.choices:
1064
1154
  from rich.panel import Panel
1155
+
1065
1156
  console.print()
1066
- console.print(Panel(
1067
- response.choices[0].message.content,
1068
- title="[bold cyan]GPT-4[/bold cyan]",
1069
- title_align="left",
1070
- border_style="dim cyan",
1071
- padding=(1, 2)
1072
- ))
1157
+ console.print(
1158
+ Panel(
1159
+ response.choices[0].message.content,
1160
+ title="[bold cyan]GPT-4[/bold cyan]",
1161
+ title_align="left",
1162
+ border_style="dim cyan",
1163
+ padding=(1, 2),
1164
+ )
1165
+ )
1073
1166
  return
1074
-
1167
+
1075
1168
  except Exception as e:
1076
1169
  console.print(f"[yellow]OpenAI error: {e}[/yellow]")
1077
-
1170
+
1078
1171
  # Try Anthropic
1079
1172
  if os.getenv("ANTHROPIC_API_KEY"):
1080
1173
  try:
1081
1174
  from anthropic import AsyncAnthropic
1082
-
1175
+
1083
1176
  client = AsyncAnthropic()
1084
1177
  response = await client.messages.create(
1085
1178
  model="claude-3-5-sonnet-20241022",
1086
1179
  messages=[{"role": "user", "content": message}],
1087
- max_tokens=2000
1180
+ max_tokens=2000,
1088
1181
  )
1089
-
1182
+
1090
1183
  if response.content:
1091
1184
  from rich.panel import Panel
1185
+
1092
1186
  console.print()
1093
- console.print(Panel(
1094
- response.content[0].text,
1095
- title="[bold cyan]Claude[/bold cyan]",
1096
- title_align="left",
1097
- border_style="dim cyan",
1098
- padding=(1, 2)
1099
- ))
1187
+ console.print(
1188
+ Panel(
1189
+ response.content[0].text,
1190
+ title="[bold cyan]Claude[/bold cyan]",
1191
+ title_align="left",
1192
+ border_style="dim cyan",
1193
+ padding=(1, 2),
1194
+ )
1195
+ )
1100
1196
  return
1101
-
1197
+
1102
1198
  except Exception as e:
1103
1199
  console.print(f"[yellow]Anthropic error: {e}[/yellow]")
1104
-
1200
+
1105
1201
  # No API keys available
1106
1202
  console.print("[red]No AI API keys configured![/red]")
1107
- console.print("[yellow]Try these options that don't need your API key:[/yellow]")
1203
+ console.print(
1204
+ "[yellow]Try these options that don't need your API key:[/yellow]"
1205
+ )
1108
1206
  console.print("\n[bold]CLI Tools (use existing tools):[/bold]")
1109
- console.print(" • hanzo dev --orchestrator codex # OpenAI CLI (if installed)")
1110
- console.print(" • hanzo dev --orchestrator claude # Claude Desktop (if installed)")
1111
- console.print(" • hanzo dev --orchestrator gemini # Gemini CLI (if installed)")
1112
- console.print(" • hanzo dev --orchestrator hanzo-ide # Hanzo IDE from ~/work/hanzo/ide")
1207
+ console.print(
1208
+ " • hanzo dev --orchestrator codex # OpenAI CLI (if installed)"
1209
+ )
1210
+ console.print(
1211
+ " • hanzo dev --orchestrator claude # Claude Desktop (if installed)"
1212
+ )
1213
+ console.print(
1214
+ " • hanzo dev --orchestrator gemini # Gemini CLI (if installed)"
1215
+ )
1216
+ console.print(
1217
+ " • hanzo dev --orchestrator hanzo-ide # Hanzo IDE from ~/work/hanzo/ide"
1218
+ )
1113
1219
  console.print("\n[bold]Free APIs (rate limited):[/bold]")
1114
- console.print(" • hanzo dev --orchestrator codestral # Free Mistral Codestral")
1220
+ console.print(
1221
+ " • hanzo dev --orchestrator codestral # Free Mistral Codestral"
1222
+ )
1115
1223
  console.print(" • hanzo dev --orchestrator starcoder # Free StarCoder")
1116
1224
  console.print("\n[bold]Local Models (unlimited):[/bold]")
1117
1225
  console.print(" • hanzo dev --orchestrator local:llama3.2 # Via Ollama")
@@ -1120,14 +1228,14 @@ Examples:
1120
1228
  console.print("\n[dim]Or set API keys for full access:[/dim]")
1121
1229
  console.print(" • export OPENAI_API_KEY=sk-...")
1122
1230
  console.print(" • export ANTHROPIC_API_KEY=sk-ant-...")
1123
-
1231
+
1124
1232
  async def _use_free_codestral(self, message: str):
1125
1233
  """Use free Mistral Codestral API (no API key needed for trial)."""
1126
1234
  try:
1127
1235
  import httpx
1128
-
1236
+
1129
1237
  console.print("[dim]Using free Codestral API (rate limited)...[/dim]")
1130
-
1238
+
1131
1239
  async with httpx.AsyncClient() as client:
1132
1240
  # Mistral offers free tier with rate limits
1133
1241
  response = await client.post(
@@ -1139,37 +1247,46 @@ Examples:
1139
1247
  json={
1140
1248
  "model": "codestral-latest",
1141
1249
  "messages": [
1142
- {"role": "system", "content": "You are Codestral, an AI coding assistant."},
1143
- {"role": "user", "content": message}
1250
+ {
1251
+ "role": "system",
1252
+ "content": "You are Codestral, an AI coding assistant.",
1253
+ },
1254
+ {"role": "user", "content": message},
1144
1255
  ],
1145
1256
  "temperature": 0.7,
1146
- "max_tokens": 2000
1257
+ "max_tokens": 2000,
1147
1258
  },
1148
- timeout=30.0
1259
+ timeout=30.0,
1149
1260
  )
1150
-
1261
+
1151
1262
  if response.status_code == 200:
1152
1263
  data = response.json()
1153
1264
  if data.get("choices"):
1154
- console.print(f"[cyan]Codestral:[/cyan] {data['choices'][0]['message']['content']}")
1265
+ console.print(
1266
+ f"[cyan]Codestral:[/cyan] {data['choices'][0]['message']['content']}"
1267
+ )
1155
1268
  else:
1156
- console.print("[yellow]Free tier limit reached. Try local models instead:[/yellow]")
1157
- console.print(" Install Ollama: curl -fsSL https://ollama.com/install.sh | sh")
1269
+ console.print(
1270
+ "[yellow]Free tier limit reached. Try local models instead:[/yellow]"
1271
+ )
1272
+ console.print(
1273
+ " • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
1274
+ )
1158
1275
  console.print(" • Run: ollama pull codellama")
1159
1276
  console.print(" • Use: hanzo dev --orchestrator local:codellama")
1160
-
1277
+
1161
1278
  except Exception as e:
1162
1279
  console.print(f"[red]Codestral error: {e}[/red]")
1163
1280
  console.print("[yellow]Try local models instead (no limits):[/yellow]")
1164
1281
  console.print(" • hanzo dev --orchestrator local:codellama")
1165
-
1282
+
1166
1283
  async def _use_free_starcoder(self, message: str):
1167
1284
  """Use free StarCoder via HuggingFace Inference API."""
1168
1285
  try:
1169
1286
  import httpx
1170
-
1287
+
1171
1288
  console.print("[dim]Using free StarCoder API...[/dim]")
1172
-
1289
+
1173
1290
  async with httpx.AsyncClient() as client:
1174
1291
  # HuggingFace offers free inference API
1175
1292
  response = await client.post(
@@ -1182,33 +1299,37 @@ Examples:
1182
1299
  "parameters": {
1183
1300
  "temperature": 0.7,
1184
1301
  "max_new_tokens": 2000,
1185
- "return_full_text": False
1186
- }
1302
+ "return_full_text": False,
1303
+ },
1187
1304
  },
1188
- timeout=30.0
1305
+ timeout=30.0,
1189
1306
  )
1190
-
1307
+
1191
1308
  if response.status_code == 200:
1192
1309
  data = response.json()
1193
1310
  if isinstance(data, list) and data:
1194
- console.print(f"[cyan]StarCoder:[/cyan] {data[0].get('generated_text', '')}")
1311
+ console.print(
1312
+ f"[cyan]StarCoder:[/cyan] {data[0].get('generated_text', '')}"
1313
+ )
1195
1314
  else:
1196
- console.print("[yellow]API limit reached. Install local models:[/yellow]")
1315
+ console.print(
1316
+ "[yellow]API limit reached. Install local models:[/yellow]"
1317
+ )
1197
1318
  console.print(" • brew install ollama")
1198
1319
  console.print(" • ollama pull starcoder2")
1199
1320
  console.print(" • hanzo dev --orchestrator local:starcoder2")
1200
-
1321
+
1201
1322
  except Exception as e:
1202
1323
  console.print(f"[red]StarCoder error: {e}[/red]")
1203
-
1324
+
1204
1325
  async def _use_openai_cli(self, message: str):
1205
1326
  """Use OpenAI CLI (Codex) - the official OpenAI CLI tool."""
1206
1327
  try:
1207
1328
  import json
1208
1329
  import subprocess
1209
-
1330
+
1210
1331
  console.print("[dim]Using OpenAI CLI (Codex)...[/dim]")
1211
-
1332
+
1212
1333
  # Check if openai CLI is installed
1213
1334
  result = subprocess.run(["which", "openai"], capture_output=True, text=True)
1214
1335
  if result.returncode != 0:
@@ -1218,37 +1339,42 @@ Examples:
1218
1339
  console.print(" • openai login")
1219
1340
  console.print("Then use: hanzo dev --orchestrator codex")
1220
1341
  return
1221
-
1342
+
1222
1343
  # Use openai CLI to chat - correct syntax
1223
- cmd = ["openai", "api", "chat.completions.create", "-m", "gpt-4", "-g", message]
1224
-
1344
+ cmd = [
1345
+ "openai",
1346
+ "api",
1347
+ "chat.completions.create",
1348
+ "-m",
1349
+ "gpt-4",
1350
+ "-g",
1351
+ message,
1352
+ ]
1353
+
1225
1354
  process = subprocess.Popen(
1226
- cmd,
1227
- stdout=subprocess.PIPE,
1228
- stderr=subprocess.PIPE,
1229
- text=True
1355
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
1230
1356
  )
1231
-
1357
+
1232
1358
  stdout, stderr = process.communicate(timeout=30)
1233
-
1359
+
1234
1360
  if process.returncode == 0 and stdout:
1235
1361
  console.print(f"[cyan]Codex:[/cyan] {stdout.strip()}")
1236
1362
  else:
1237
1363
  console.print(f"[red]OpenAI CLI error: {stderr}[/red]")
1238
-
1364
+
1239
1365
  except subprocess.TimeoutExpired:
1240
1366
  console.print("[yellow]OpenAI CLI timed out[/yellow]")
1241
1367
  except Exception as e:
1242
1368
  console.print(f"[red]Error using OpenAI CLI: {e}[/red]")
1243
-
1369
+
1244
1370
  async def _use_claude_cli(self, message: str):
1245
1371
  """Use Claude Desktop/Code CLI."""
1246
1372
  try:
1247
1373
  import os
1248
1374
  import subprocess
1249
-
1375
+
1250
1376
  console.print("[dim]Using Claude Desktop...[/dim]")
1251
-
1377
+
1252
1378
  # Check for Claude Code or Claude Desktop
1253
1379
  claude_paths = [
1254
1380
  "/usr/local/bin/claude",
@@ -1256,13 +1382,17 @@ Examples:
1256
1382
  os.path.expanduser("~/Applications/Claude.app/Contents/MacOS/Claude"),
1257
1383
  "claude", # In PATH
1258
1384
  ]
1259
-
1385
+
1260
1386
  claude_path = None
1261
1387
  for path in claude_paths:
1262
- if os.path.exists(path) or subprocess.run(["which", path], capture_output=True).returncode == 0:
1388
+ if (
1389
+ os.path.exists(path)
1390
+ or subprocess.run(["which", path], capture_output=True).returncode
1391
+ == 0
1392
+ ):
1263
1393
  claude_path = path
1264
1394
  break
1265
-
1395
+
1266
1396
  if not claude_path:
1267
1397
  console.print("[red]Claude Desktop not found![/red]")
1268
1398
  console.print("[yellow]To install:[/yellow]")
@@ -1270,11 +1400,11 @@ Examples:
1270
1400
  console.print(" • Or: brew install --cask claude")
1271
1401
  console.print("Then use: hanzo dev --orchestrator claude")
1272
1402
  return
1273
-
1403
+
1274
1404
  # Send message to Claude via CLI or AppleScript on macOS
1275
1405
  if sys.platform == "darwin":
1276
1406
  # Use AppleScript to interact with Claude Desktop
1277
- script = f'''
1407
+ script = f"""
1278
1408
  tell application "Claude"
1279
1409
  activate
1280
1410
  delay 0.5
@@ -1283,34 +1413,36 @@ Examples:
1283
1413
  key code 36 -- Enter key
1284
1414
  end tell
1285
1415
  end tell
1286
- '''
1287
-
1416
+ """
1417
+
1288
1418
  subprocess.run(["osascript", "-e", script])
1289
- console.print("[cyan]Sent to Claude Desktop. Check the app for response.[/cyan]")
1419
+ console.print(
1420
+ "[cyan]Sent to Claude Desktop. Check the app for response.[/cyan]"
1421
+ )
1290
1422
  else:
1291
1423
  # Try direct CLI invocation
1292
1424
  process = subprocess.Popen(
1293
1425
  [claude_path, "--message", message],
1294
1426
  stdout=subprocess.PIPE,
1295
1427
  stderr=subprocess.PIPE,
1296
- text=True
1428
+ text=True,
1297
1429
  )
1298
-
1430
+
1299
1431
  stdout, stderr = process.communicate(timeout=30)
1300
-
1432
+
1301
1433
  if stdout:
1302
1434
  console.print(f"[cyan]Claude:[/cyan] {stdout.strip()}")
1303
-
1435
+
1304
1436
  except Exception as e:
1305
1437
  console.print(f"[red]Error using Claude Desktop: {e}[/red]")
1306
-
1438
+
1307
1439
  async def _use_gemini_cli(self, message: str):
1308
1440
  """Use Gemini CLI."""
1309
1441
  try:
1310
1442
  import subprocess
1311
-
1443
+
1312
1444
  console.print("[dim]Using Gemini CLI...[/dim]")
1313
-
1445
+
1314
1446
  # Check if gemini CLI is installed
1315
1447
  result = subprocess.run(["which", "gemini"], capture_output=True, text=True)
1316
1448
  if result.returncode != 0:
@@ -1321,47 +1453,46 @@ Examples:
1321
1453
  console.print(" • Set GOOGLE_API_KEY environment variable")
1322
1454
  console.print("Then use: hanzo dev --orchestrator gemini")
1323
1455
  return
1324
-
1456
+
1325
1457
  # Use gemini CLI
1326
1458
  cmd = ["gemini", "chat", message]
1327
-
1459
+
1328
1460
  process = subprocess.Popen(
1329
- cmd,
1330
- stdout=subprocess.PIPE,
1331
- stderr=subprocess.PIPE,
1332
- text=True
1461
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
1333
1462
  )
1334
-
1463
+
1335
1464
  stdout, stderr = process.communicate(timeout=30)
1336
-
1465
+
1337
1466
  if process.returncode == 0 and stdout:
1338
1467
  console.print(f"[cyan]Gemini:[/cyan] {stdout.strip()}")
1339
1468
  else:
1340
1469
  console.print(f"[red]Gemini CLI error: {stderr}[/red]")
1341
-
1470
+
1342
1471
  except subprocess.TimeoutExpired:
1343
1472
  console.print("[yellow]Gemini CLI timed out[/yellow]")
1344
1473
  except Exception as e:
1345
1474
  console.print(f"[red]Error using Gemini CLI: {e}[/red]")
1346
-
1475
+
1347
1476
  async def _use_hanzo_ide(self, message: str):
1348
1477
  """Use Hanzo Dev IDE from ~/work/hanzo/ide."""
1349
1478
  try:
1350
1479
  import os
1351
1480
  import subprocess
1352
-
1481
+
1353
1482
  console.print("[dim]Using Hanzo Dev IDE...[/dim]")
1354
-
1483
+
1355
1484
  # Check if Hanzo IDE exists
1356
1485
  ide_path = os.path.expanduser("~/work/hanzo/ide")
1357
1486
  if not os.path.exists(ide_path):
1358
1487
  console.print("[red]Hanzo Dev IDE not found![/red]")
1359
1488
  console.print("[yellow]Expected location: ~/work/hanzo/ide[/yellow]")
1360
1489
  console.print("To set up:")
1361
- console.print(" • git clone https://github.com/hanzoai/ide ~/work/hanzo/ide")
1490
+ console.print(
1491
+ " • git clone https://github.com/hanzoai/ide ~/work/hanzo/ide"
1492
+ )
1362
1493
  console.print(" • cd ~/work/hanzo/ide && npm install")
1363
1494
  return
1364
-
1495
+
1365
1496
  # Check for the CLI entry point
1366
1497
  cli_paths = [
1367
1498
  os.path.join(ide_path, "bin", "hanzo-ide"),
@@ -1369,13 +1500,13 @@ Examples:
1369
1500
  os.path.join(ide_path, "cli.js"),
1370
1501
  os.path.join(ide_path, "index.js"),
1371
1502
  ]
1372
-
1503
+
1373
1504
  cli_path = None
1374
1505
  for path in cli_paths:
1375
1506
  if os.path.exists(path):
1376
1507
  cli_path = path
1377
1508
  break
1378
-
1509
+
1379
1510
  if not cli_path:
1380
1511
  # Try to run with npm/node
1381
1512
  package_json = os.path.join(ide_path, "package.json")
@@ -1393,17 +1524,13 @@ Examples:
1393
1524
  else:
1394
1525
  cmd = [cli_path, "chat", message]
1395
1526
  cwd = None
1396
-
1527
+
1397
1528
  process = subprocess.Popen(
1398
- cmd,
1399
- stdout=subprocess.PIPE,
1400
- stderr=subprocess.PIPE,
1401
- text=True,
1402
- cwd=cwd
1529
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd
1403
1530
  )
1404
-
1531
+
1405
1532
  stdout, stderr = process.communicate(timeout=30)
1406
-
1533
+
1407
1534
  if process.returncode == 0 and stdout:
1408
1535
  console.print(f"[cyan]Hanzo IDE:[/cyan] {stdout.strip()}")
1409
1536
  else:
@@ -1411,73 +1538,83 @@ Examples:
1411
1538
  console.print(f"[yellow]Hanzo IDE: {stderr}[/yellow]")
1412
1539
  else:
1413
1540
  console.print("[yellow]Hanzo IDE: No response[/yellow]")
1414
-
1541
+
1415
1542
  except subprocess.TimeoutExpired:
1416
1543
  console.print("[yellow]Hanzo IDE timed out[/yellow]")
1417
1544
  except Exception as e:
1418
1545
  console.print(f"[red]Error using Hanzo IDE: {e}[/red]")
1419
-
1546
+
1420
1547
  async def _use_local_model(self, message: str):
1421
1548
  """Use local model via Ollama or LM Studio."""
1422
1549
  import httpx
1423
-
1550
+
1424
1551
  model_name = self.orchestrator.orchestrator_model.replace("local:", "")
1425
-
1552
+
1426
1553
  # Try Ollama first (default port 11434)
1427
1554
  try:
1428
1555
  console.print(f"[dim]Using local {model_name} via Ollama...[/dim]")
1429
-
1556
+
1430
1557
  async with httpx.AsyncClient() as client:
1431
1558
  response = await client.post(
1432
1559
  "http://localhost:11434/api/chat",
1433
1560
  json={
1434
1561
  "model": model_name,
1435
1562
  "messages": [
1436
- {"role": "system", "content": "You are a helpful AI coding assistant."},
1437
- {"role": "user", "content": message}
1563
+ {
1564
+ "role": "system",
1565
+ "content": "You are a helpful AI coding assistant.",
1566
+ },
1567
+ {"role": "user", "content": message},
1438
1568
  ],
1439
- "stream": False
1569
+ "stream": False,
1440
1570
  },
1441
- timeout=60.0
1571
+ timeout=60.0,
1442
1572
  )
1443
-
1573
+
1444
1574
  if response.status_code == 200:
1445
1575
  data = response.json()
1446
1576
  if data.get("message"):
1447
- console.print(f"[cyan]{model_name}:[/cyan] {data['message']['content']}")
1577
+ console.print(
1578
+ f"[cyan]{model_name}:[/cyan] {data['message']['content']}"
1579
+ )
1448
1580
  return
1449
-
1581
+
1450
1582
  except Exception:
1451
1583
  pass
1452
-
1584
+
1453
1585
  # Try LM Studio (default port 1234)
1454
1586
  try:
1455
1587
  console.print(f"[dim]Trying LM Studio...[/dim]")
1456
-
1588
+
1457
1589
  async with httpx.AsyncClient() as client:
1458
1590
  response = await client.post(
1459
1591
  "http://localhost:1234/v1/chat/completions",
1460
1592
  json={
1461
1593
  "model": model_name,
1462
1594
  "messages": [
1463
- {"role": "system", "content": "You are a helpful AI coding assistant."},
1464
- {"role": "user", "content": message}
1595
+ {
1596
+ "role": "system",
1597
+ "content": "You are a helpful AI coding assistant.",
1598
+ },
1599
+ {"role": "user", "content": message},
1465
1600
  ],
1466
1601
  "temperature": 0.7,
1467
- "max_tokens": 2000
1602
+ "max_tokens": 2000,
1468
1603
  },
1469
- timeout=60.0
1604
+ timeout=60.0,
1470
1605
  )
1471
-
1606
+
1472
1607
  if response.status_code == 200:
1473
1608
  data = response.json()
1474
1609
  if data.get("choices"):
1475
- console.print(f"[cyan]{model_name}:[/cyan] {data['choices'][0]['message']['content']}")
1610
+ console.print(
1611
+ f"[cyan]{model_name}:[/cyan] {data['choices'][0]['message']['content']}"
1612
+ )
1476
1613
  return
1477
-
1614
+
1478
1615
  except Exception:
1479
1616
  pass
1480
-
1617
+
1481
1618
  # Neither worked
1482
1619
  console.print(f"[red]Local model '{model_name}' not available[/red]")
1483
1620
  console.print("[yellow]To use local models:[/yellow]")
@@ -1489,13 +1626,13 @@ Examples:
1489
1626
  console.print(" • Download from https://lmstudio.ai")
1490
1627
  console.print(f" • Load {model_name} model")
1491
1628
  console.print(" • Start local server (port 1234)")
1492
-
1629
+
1493
1630
  async def handle_memory_command(self, command: str):
1494
1631
  """Handle memory/context commands starting with #."""
1495
1632
  parts = command.split(maxsplit=1)
1496
1633
  cmd = parts[0].lower() if parts else ""
1497
1634
  args = parts[1] if len(parts) > 1 else ""
1498
-
1635
+
1499
1636
  if cmd == "remember":
1500
1637
  if args:
1501
1638
  console.print(f"[green]✓ Remembered: {args}[/green]")
@@ -1578,13 +1715,10 @@ async def run_dev_orchestrator(**kwargs):
1578
1715
  console_obj.print("[red]Failed to initialize network[/red]")
1579
1716
  return
1580
1717
  else:
1581
- # Fallback to multi-Claude mode
1582
- console_obj.print(f"[cyan]Mode: Multi-Claude Orchestration (legacy)[/cyan]")
1583
- console_obj.print(
1584
- f"Instances: {instances} (1 primary + {instances-1} critic{'s' if instances > 2 else ''})"
1585
- )
1718
+ # Fallback to API mode
1719
+ console_obj.print(f"[cyan]Mode: AI Chat[/cyan]")
1720
+ console_obj.print(f"Model: {orchestrator_model}")
1586
1721
  console_obj.print(f"MCP Tools: {'Enabled' if mcp_tools else 'Disabled'}")
1587
- console_obj.print(f"Networking: {'Enabled' if network_mode else 'Disabled'}")
1588
1722
  console_obj.print(f"Guardrails: {'Enabled' if guardrails else 'Disabled'}\n")
1589
1723
 
1590
1724
  orchestrator = MultiClaudeOrchestrator(
@@ -2216,6 +2350,22 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2216
2350
 
2217
2351
  async def initialize(self):
2218
2352
  """Initialize all Claude instances with MCP networking."""
2353
+ # Check if Claude is available first
2354
+ claude_available = False
2355
+ try:
2356
+ import shutil
2357
+
2358
+ if self.claude_code_path and Path(self.claude_code_path).exists():
2359
+ claude_available = True
2360
+ elif shutil.which("claude"):
2361
+ claude_available = True
2362
+ except:
2363
+ pass
2364
+
2365
+ if not claude_available:
2366
+ # Skip Claude instance initialization - will use API fallback silently
2367
+ return
2368
+
2219
2369
  self.console.print("[cyan]Initializing Claude instances...[/cyan]")
2220
2370
 
2221
2371
  for i in range(self.num_instances):
@@ -2237,7 +2387,8 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2237
2387
  if success:
2238
2388
  self.console.print(f"[green]✓ Instance {i} started[/green]")
2239
2389
  else:
2240
- self.console.print(f"[red]✗ Failed to start instance {i}[/red]")
2390
+ # Don't show error, just skip silently
2391
+ pass
2241
2392
 
2242
2393
  async def _create_instance_config(self, index: int, role: str) -> Dict:
2243
2394
  """Create configuration for a Claude instance."""
@@ -2377,7 +2528,13 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2377
2528
 
2378
2529
  # Check if instances are initialized
2379
2530
  if not self.claude_instances:
2380
- # No instances started, use direct API
2531
+ # No instances started, use fallback handler for smart routing
2532
+ from .fallback_handler import smart_chat
2533
+
2534
+ response = await smart_chat(task, console=self.console)
2535
+ if response:
2536
+ return {"output": response, "success": True}
2537
+ # If smart_chat fails, try direct API as last resort
2381
2538
  return await self._call_api_model(task)
2382
2539
 
2383
2540
  # Step 1: Primary execution
@@ -2454,113 +2611,158 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2454
2611
  else:
2455
2612
  # Try API-based models
2456
2613
  return await self._call_api_model(prompt)
2457
-
2614
+
2458
2615
  async def _call_openai_cli(self, prompt: str) -> Dict:
2459
2616
  """Call OpenAI CLI and return structured response."""
2460
2617
  try:
2461
2618
  import subprocess
2619
+
2462
2620
  result = subprocess.run(
2463
- ["openai", "api", "chat.completions.create", "-m", "gpt-4", "-g", prompt],
2621
+ [
2622
+ "openai",
2623
+ "api",
2624
+ "chat.completions.create",
2625
+ "-m",
2626
+ "gpt-4",
2627
+ "-g",
2628
+ prompt,
2629
+ ],
2464
2630
  capture_output=True,
2465
2631
  text=True,
2466
- timeout=30
2632
+ timeout=30,
2467
2633
  )
2468
2634
  if result.returncode == 0 and result.stdout:
2469
2635
  return {"output": result.stdout.strip(), "success": True}
2470
2636
  except Exception as e:
2471
2637
  logger.error(f"OpenAI CLI error: {e}")
2472
- return {"output": "OpenAI CLI not available. Install with: pip install openai-cli", "success": False}
2473
-
2638
+ return {
2639
+ "output": "OpenAI CLI not available. Install with: pip install openai-cli",
2640
+ "success": False,
2641
+ }
2642
+
2474
2643
  async def _call_claude_cli(self, prompt: str) -> Dict:
2475
2644
  """Call Claude Desktop and return structured response."""
2476
2645
  try:
2477
2646
  import sys
2478
2647
  import subprocess
2648
+
2479
2649
  if sys.platform == "darwin":
2480
2650
  # macOS - use AppleScript
2481
2651
  script = f'tell application "Claude" to activate'
2482
2652
  subprocess.run(["osascript", "-e", script])
2483
- return {"output": "Sent to Claude Desktop. Check app for response.", "success": True}
2653
+ return {
2654
+ "output": "Sent to Claude Desktop. Check app for response.",
2655
+ "success": True,
2656
+ }
2484
2657
  except Exception as e:
2485
2658
  logger.error(f"Claude CLI error: {e}")
2486
- return {"output": "Claude Desktop not available. Install from https://claude.ai/desktop", "success": False}
2487
-
2659
+ return {
2660
+ "output": "Claude Desktop not available. Install from https://claude.ai/desktop",
2661
+ "success": False,
2662
+ }
2663
+
2488
2664
  async def _call_gemini_cli(self, prompt: str) -> Dict:
2489
2665
  """Call Gemini CLI and return structured response."""
2490
2666
  try:
2491
2667
  import subprocess
2668
+
2492
2669
  result = subprocess.run(
2493
- ["gemini", "chat", prompt],
2494
- capture_output=True,
2495
- text=True,
2496
- timeout=30
2670
+ ["gemini", "chat", prompt], capture_output=True, text=True, timeout=30
2497
2671
  )
2498
2672
  if result.returncode == 0 and result.stdout:
2499
2673
  return {"output": result.stdout.strip(), "success": True}
2500
2674
  except Exception as e:
2501
2675
  logger.error(f"Gemini CLI error: {e}")
2502
- return {"output": "Gemini CLI not available. Install with: pip install google-generativeai-cli", "success": False}
2503
-
2676
+ return {
2677
+ "output": "Gemini CLI not available. Install with: pip install google-generativeai-cli",
2678
+ "success": False,
2679
+ }
2680
+
2504
2681
  async def _call_local_model(self, prompt: str) -> Dict:
2505
2682
  """Call local model via Ollama and return structured response."""
2506
2683
  try:
2507
2684
  import httpx
2685
+
2508
2686
  model_name = self.orchestrator_model.replace("local:", "")
2509
-
2687
+
2510
2688
  async with httpx.AsyncClient() as client:
2511
2689
  response = await client.post(
2512
2690
  "http://localhost:11434/api/chat",
2513
2691
  json={
2514
2692
  "model": model_name,
2515
2693
  "messages": [{"role": "user", "content": prompt}],
2516
- "stream": False
2694
+ "stream": False,
2517
2695
  },
2518
- timeout=60.0
2696
+ timeout=60.0,
2519
2697
  )
2520
-
2698
+
2521
2699
  if response.status_code == 200:
2522
2700
  data = response.json()
2523
2701
  if data.get("message"):
2524
2702
  return {"output": data["message"]["content"], "success": True}
2525
2703
  except Exception as e:
2526
2704
  logger.error(f"Local model error: {e}")
2527
- return {"output": f"Local model not available. Install Ollama and run: ollama pull {self.orchestrator_model.replace('local:', '')}", "success": False}
2528
-
2705
+ return {
2706
+ "output": f"Local model not available. Install Ollama and run: ollama pull {self.orchestrator_model.replace('local:', '')}",
2707
+ "success": False,
2708
+ }
2709
+
2529
2710
  async def _call_api_model(self, prompt: str) -> Dict:
2530
2711
  """Call API-based model and return structured response."""
2531
2712
  import os
2532
-
2533
- # Try OpenAI
2534
- if os.getenv("OPENAI_API_KEY"):
2713
+
2714
+ # Try OpenAI first (check environment variable properly)
2715
+ openai_key = os.environ.get("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY")
2716
+ if openai_key:
2535
2717
  try:
2536
2718
  from openai import AsyncOpenAI
2537
- client = AsyncOpenAI()
2719
+
2720
+ client = AsyncOpenAI(api_key=openai_key)
2538
2721
  response = await client.chat.completions.create(
2539
2722
  model="gpt-4",
2540
2723
  messages=[{"role": "user", "content": prompt}],
2541
- max_tokens=2000
2724
+ max_tokens=2000,
2542
2725
  )
2543
2726
  if response.choices:
2544
- return {"output": response.choices[0].message.content, "success": True}
2727
+ return {
2728
+ "output": response.choices[0].message.content,
2729
+ "success": True,
2730
+ }
2545
2731
  except Exception as e:
2546
2732
  logger.error(f"OpenAI API error: {e}")
2547
-
2733
+
2548
2734
  # Try Anthropic
2549
- if os.getenv("ANTHROPIC_API_KEY"):
2735
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY") or os.getenv(
2736
+ "ANTHROPIC_API_KEY"
2737
+ )
2738
+ if anthropic_key:
2550
2739
  try:
2551
2740
  from anthropic import AsyncAnthropic
2552
- client = AsyncAnthropic()
2741
+
2742
+ client = AsyncAnthropic(api_key=anthropic_key)
2553
2743
  response = await client.messages.create(
2554
2744
  model="claude-3-5-sonnet-20241022",
2555
2745
  messages=[{"role": "user", "content": prompt}],
2556
- max_tokens=2000
2746
+ max_tokens=2000,
2557
2747
  )
2558
2748
  if response.content:
2559
2749
  return {"output": response.content[0].text, "success": True}
2560
2750
  except Exception as e:
2561
2751
  logger.error(f"Anthropic API error: {e}")
2562
-
2563
- return {"output": "No API keys configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY", "success": False}
2752
+
2753
+ # Try fallback handler as last resort
2754
+ from .fallback_handler import smart_chat
2755
+
2756
+ response = await smart_chat(
2757
+ prompt, console=None
2758
+ ) # No console to avoid duplicate messages
2759
+ if response:
2760
+ return {"output": response, "success": True}
2761
+
2762
+ return {
2763
+ "output": "No API keys configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY",
2764
+ "success": False,
2765
+ }
2564
2766
 
2565
2767
  async def _validate_improvement(self, original: Dict, improved: Dict) -> bool:
2566
2768
  """Validate that an improvement doesn't degrade quality."""