hanzo 0.3.20__tar.gz → 0.3.22__tar.gz

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.

Files changed (34) hide show
  1. {hanzo-0.3.20 → hanzo-0.3.22}/PKG-INFO +1 -1
  2. {hanzo-0.3.20 → hanzo-0.3.22}/pyproject.toml +1 -1
  3. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/cli.py +1 -1
  4. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/dev.py +79 -42
  5. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/fallback_handler.py +14 -9
  6. hanzo-0.3.22/src/hanzo/rate_limiter.py +332 -0
  7. hanzo-0.3.22/src/hanzo/streaming.py +271 -0
  8. {hanzo-0.3.20 → hanzo-0.3.22}/.gitignore +0 -0
  9. {hanzo-0.3.20 → hanzo-0.3.22}/README.md +0 -0
  10. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/__init__.py +0 -0
  11. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/__main__.py +0 -0
  12. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/__init__.py +0 -0
  13. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/agent.py +0 -0
  14. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/auth.py +0 -0
  15. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/chat.py +0 -0
  16. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/cluster.py +0 -0
  17. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/config.py +0 -0
  18. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/mcp.py +0 -0
  19. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/miner.py +0 -0
  20. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/network.py +0 -0
  21. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/repl.py +0 -0
  22. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/commands/tools.py +0 -0
  23. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/interactive/__init__.py +0 -0
  24. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/interactive/dashboard.py +0 -0
  25. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/interactive/repl.py +0 -0
  26. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/mcp_server.py +0 -0
  27. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/memory_manager.py +0 -0
  28. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/orchestrator_config.py +0 -0
  29. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/repl.py +0 -0
  30. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/router/__init__.py +0 -0
  31. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/utils/__init__.py +0 -0
  32. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/utils/config.py +0 -0
  33. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/utils/net_check.py +0 -0
  34. {hanzo-0.3.20 → hanzo-0.3.22}/src/hanzo/utils/output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hanzo
3
- Version: 0.3.20
3
+ Version: 0.3.22
4
4
  Summary: Hanzo AI - Complete AI Infrastructure Platform with CLI, Router, MCP, and Agent Runtime
5
5
  Project-URL: Homepage, https://hanzo.ai
6
6
  Project-URL: Repository, https://github.com/hanzoai/python-sdk
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hanzo"
3
- version = "0.3.20"
3
+ version = "0.3.22"
4
4
  description = "Hanzo AI - Complete AI Infrastructure Platform with CLI, Router, MCP, and Agent Runtime"
5
5
  authors = [
6
6
  {name = "Hanzo AI", email = "dev@hanzo.ai"},
@@ -26,7 +26,7 @@ from .utils.output import console
26
26
  from .interactive.repl import HanzoREPL
27
27
 
28
28
  # Version
29
- __version__ = "0.3.20"
29
+ __version__ = "0.3.22"
30
30
 
31
31
 
32
32
  @click.group(invoke_without_command=True)
@@ -697,34 +697,35 @@ class HanzoDevREPL:
697
697
  padding=(0, 1)
698
698
  ))
699
699
  console.print()
700
+
701
+ # Check for available API keys and show status
702
+ from .fallback_handler import FallbackHandler
703
+ handler = FallbackHandler()
704
+ if not handler.fallback_order:
705
+ console.print("[yellow]⚠️ No API keys detected[/yellow]")
706
+ console.print("[dim]Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI[/dim]")
707
+ console.print()
708
+ else:
709
+ primary = handler.fallback_order[0][1]
710
+ console.print(f"[green]✅ Using {primary} for AI responses[/green]")
711
+ console.print()
700
712
 
701
713
  while True:
702
714
  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
-
715
+ # Simple prompt without box borders to avoid rendering issues
709
716
  try:
710
- # Get input - using simple input() wrapped in executor for async
711
- # The visual box is drawn by console.print statements
717
+ # Add spacing to prevent UI cutoff at bottom
712
718
  user_input = await asyncio.get_event_loop().run_in_executor(
713
719
  None,
714
720
  input,
715
- '› ' # Using › instead of > for a more modern look
721
+ '› ' # Clean prompt
716
722
  )
717
-
718
- # Draw input box border (bottom)
719
- console.print("[dim white]╰" + "─" * 78 + "╯[/dim white]")
723
+ console.print() # Add spacing after input
720
724
 
721
725
  except EOFError:
722
726
  console.print() # New line before exit
723
- console.print("[dim white]╰" + "─" * 78 + "╯[/dim white]")
724
727
  break
725
728
  except KeyboardInterrupt:
726
- console.print() # Complete the box
727
- console.print("[dim white]╰" + "─" * 78 + "╯[/dim white]")
728
729
  console.print("\n[dim yellow]Use /exit to quit[/dim]")
729
730
  continue
730
731
 
@@ -929,21 +930,14 @@ Examples:
929
930
  # Try smart fallback if no specific model configured
930
931
  if not hasattr(self.orchestrator, 'orchestrator_model') or \
931
932
  self.orchestrator.orchestrator_model == "auto":
932
- from .fallback_handler import smart_chat
933
- response = await smart_chat(enhanced_message, console)
933
+ # Use streaming if available
934
+ from .streaming import stream_with_fallback
935
+ response = await stream_with_fallback(enhanced_message, console)
936
+
934
937
  if response:
935
938
  # Save AI response to memory
936
939
  self.memory_manager.add_message("assistant", response)
937
-
938
- from rich.panel import Panel
939
- console.print()
940
- console.print(Panel(
941
- response,
942
- title="[bold cyan]AI Response[/bold cyan]",
943
- title_align="left",
944
- border_style="dim cyan",
945
- padding=(1, 2)
946
- ))
940
+ # Response already displayed by streaming handler
947
941
  return
948
942
  else:
949
943
  console.print("[red]No AI options available. Please configure API keys or install tools.[/red]")
@@ -1051,8 +1045,25 @@ Examples:
1051
1045
  await self._use_local_model(message)
1052
1046
  return
1053
1047
 
1054
- # Try OpenAI first
1055
- if os.getenv("OPENAI_API_KEY"):
1048
+ # Use the fallback handler to intelligently try available options
1049
+ from .fallback_handler import smart_chat
1050
+ response = await smart_chat(message, console=console)
1051
+
1052
+ if response:
1053
+ from rich.panel import Panel
1054
+ console.print()
1055
+ console.print(Panel(
1056
+ response,
1057
+ title="[bold cyan]AI Response[/bold cyan]",
1058
+ title_align="left",
1059
+ border_style="dim cyan",
1060
+ padding=(1, 2)
1061
+ ))
1062
+ return
1063
+
1064
+ # Try OpenAI first explicitly (in case fallback handler missed it)
1065
+ openai_key = os.environ.get("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY")
1066
+ if openai_key:
1056
1067
  try:
1057
1068
  from openai import AsyncOpenAI
1058
1069
 
@@ -1585,13 +1596,10 @@ async def run_dev_orchestrator(**kwargs):
1585
1596
  console_obj.print("[red]Failed to initialize network[/red]")
1586
1597
  return
1587
1598
  else:
1588
- # Fallback to multi-Claude mode
1589
- console_obj.print(f"[cyan]Mode: Multi-Claude Orchestration (legacy)[/cyan]")
1590
- console_obj.print(
1591
- f"Instances: {instances} (1 primary + {instances-1} critic{'s' if instances > 2 else ''})"
1592
- )
1599
+ # Fallback to API mode
1600
+ console_obj.print(f"[cyan]Mode: AI Chat[/cyan]")
1601
+ console_obj.print(f"Model: {orchestrator_model}")
1593
1602
  console_obj.print(f"MCP Tools: {'Enabled' if mcp_tools else 'Disabled'}")
1594
- console_obj.print(f"Networking: {'Enabled' if network_mode else 'Disabled'}")
1595
1603
  console_obj.print(f"Guardrails: {'Enabled' if guardrails else 'Disabled'}\n")
1596
1604
 
1597
1605
  orchestrator = MultiClaudeOrchestrator(
@@ -2223,6 +2231,21 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2223
2231
 
2224
2232
  async def initialize(self):
2225
2233
  """Initialize all Claude instances with MCP networking."""
2234
+ # Check if Claude is available first
2235
+ claude_available = False
2236
+ try:
2237
+ import shutil
2238
+ if self.claude_code_path and Path(self.claude_code_path).exists():
2239
+ claude_available = True
2240
+ elif shutil.which("claude"):
2241
+ claude_available = True
2242
+ except:
2243
+ pass
2244
+
2245
+ if not claude_available:
2246
+ # Skip Claude instance initialization - will use API fallback silently
2247
+ return
2248
+
2226
2249
  self.console.print("[cyan]Initializing Claude instances...[/cyan]")
2227
2250
 
2228
2251
  for i in range(self.num_instances):
@@ -2244,7 +2267,8 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2244
2267
  if success:
2245
2268
  self.console.print(f"[green]✓ Instance {i} started[/green]")
2246
2269
  else:
2247
- self.console.print(f"[red]✗ Failed to start instance {i}[/red]")
2270
+ # Don't show error, just skip silently
2271
+ pass
2248
2272
 
2249
2273
  async def _create_instance_config(self, index: int, role: str) -> Dict:
2250
2274
  """Create configuration for a Claude instance."""
@@ -2384,7 +2408,12 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2384
2408
 
2385
2409
  # Check if instances are initialized
2386
2410
  if not self.claude_instances:
2387
- # No instances started, use direct API
2411
+ # No instances started, use fallback handler for smart routing
2412
+ from .fallback_handler import smart_chat
2413
+ response = await smart_chat(task, console=self.console)
2414
+ if response:
2415
+ return {"output": response, "success": True}
2416
+ # If smart_chat fails, try direct API as last resort
2388
2417
  return await self._call_api_model(task)
2389
2418
 
2390
2419
  # Step 1: Primary execution
@@ -2537,11 +2566,12 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2537
2566
  """Call API-based model and return structured response."""
2538
2567
  import os
2539
2568
 
2540
- # Try OpenAI
2541
- if os.getenv("OPENAI_API_KEY"):
2569
+ # Try OpenAI first (check environment variable properly)
2570
+ openai_key = os.environ.get("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY")
2571
+ if openai_key:
2542
2572
  try:
2543
2573
  from openai import AsyncOpenAI
2544
- client = AsyncOpenAI()
2574
+ client = AsyncOpenAI(api_key=openai_key)
2545
2575
  response = await client.chat.completions.create(
2546
2576
  model="gpt-4",
2547
2577
  messages=[{"role": "user", "content": prompt}],
@@ -2553,10 +2583,11 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2553
2583
  logger.error(f"OpenAI API error: {e}")
2554
2584
 
2555
2585
  # Try Anthropic
2556
- if os.getenv("ANTHROPIC_API_KEY"):
2586
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_API_KEY")
2587
+ if anthropic_key:
2557
2588
  try:
2558
2589
  from anthropic import AsyncAnthropic
2559
- client = AsyncAnthropic()
2590
+ client = AsyncAnthropic(api_key=anthropic_key)
2560
2591
  response = await client.messages.create(
2561
2592
  model="claude-3-5-sonnet-20241022",
2562
2593
  messages=[{"role": "user", "content": prompt}],
@@ -2567,6 +2598,12 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2567
2598
  except Exception as e:
2568
2599
  logger.error(f"Anthropic API error: {e}")
2569
2600
 
2601
+ # Try fallback handler as last resort
2602
+ from .fallback_handler import smart_chat
2603
+ response = await smart_chat(prompt, console=None) # No console to avoid duplicate messages
2604
+ if response:
2605
+ return {"output": response, "success": True}
2606
+
2570
2607
  return {"output": "No API keys configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY", "success": False}
2571
2608
 
2572
2609
  async def _validate_improvement(self, original: Dict, improved: Dict) -> bool:
@@ -158,6 +158,8 @@ async def smart_chat(message: str, console=None) -> Optional[str]:
158
158
  Smart chat that automatically tries available AI options.
159
159
  Returns the AI response or None if all options fail.
160
160
  """
161
+ from .rate_limiter import smart_limiter
162
+
161
163
  handler = FallbackHandler()
162
164
 
163
165
  if console:
@@ -171,17 +173,20 @@ async def smart_chat(message: str, console=None) -> Optional[str]:
171
173
 
172
174
  option_type, model = best_option
173
175
 
174
- # Try the primary option
176
+ # Try the primary option with rate limiting
175
177
  try:
176
178
  if option_type == "openai_api":
177
- from openai import AsyncOpenAI
178
- client = AsyncOpenAI()
179
- response = await client.chat.completions.create(
180
- model="gpt-4",
181
- messages=[{"role": "user", "content": message}],
182
- max_tokens=500
183
- )
184
- return response.choices[0].message.content
179
+ async def call_openai():
180
+ from openai import AsyncOpenAI
181
+ client = AsyncOpenAI()
182
+ response = await client.chat.completions.create(
183
+ model="gpt-4",
184
+ messages=[{"role": "user", "content": message}],
185
+ max_tokens=500
186
+ )
187
+ return response.choices[0].message.content
188
+
189
+ return await smart_limiter.execute_with_limit("openai", call_openai)
185
190
 
186
191
  elif option_type == "anthropic_api":
187
192
  from anthropic import AsyncAnthropic
@@ -0,0 +1,332 @@
1
+ """
2
+ Rate limiting and error recovery for Hanzo Dev.
3
+ Prevents API overuse and handles failures gracefully.
4
+ """
5
+
6
+ import time
7
+ import asyncio
8
+ from typing import Dict, Optional, Any, Callable
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timedelta
11
+ from collections import deque
12
+ import random
13
+
14
+
15
+ @dataclass
16
+ class RateLimitConfig:
17
+ """Configuration for rate limiting."""
18
+ requests_per_minute: int = 20
19
+ requests_per_hour: int = 100
20
+ burst_size: int = 5
21
+ cooldown_seconds: int = 60
22
+ max_retries: int = 3
23
+ backoff_base: float = 2.0
24
+ jitter: bool = True
25
+
26
+
27
+ @dataclass
28
+ class RateLimitState:
29
+ """Current state of rate limiter."""
30
+ minute_requests: deque = field(default_factory=lambda: deque(maxlen=60))
31
+ hour_requests: deque = field(default_factory=lambda: deque(maxlen=3600))
32
+ last_request: Optional[datetime] = None
33
+ consecutive_errors: int = 0
34
+ total_requests: int = 0
35
+ total_errors: int = 0
36
+ is_throttled: bool = False
37
+ throttle_until: Optional[datetime] = None
38
+
39
+
40
+ class RateLimiter:
41
+ """Rate limiter with error recovery."""
42
+
43
+ def __init__(self, config: RateLimitConfig = None):
44
+ """Initialize rate limiter."""
45
+ self.config = config or RateLimitConfig()
46
+ self.states: Dict[str, RateLimitState] = {}
47
+
48
+ def get_state(self, key: str = "default") -> RateLimitState:
49
+ """Get or create state for a key."""
50
+ if key not in self.states:
51
+ self.states[key] = RateLimitState()
52
+ return self.states[key]
53
+
54
+ async def check_rate_limit(self, key: str = "default") -> tuple[bool, float]:
55
+ """
56
+ Check if request is allowed.
57
+ Returns (allowed, wait_seconds).
58
+ """
59
+ state = self.get_state(key)
60
+ now = datetime.now()
61
+
62
+ # Check if throttled
63
+ if state.is_throttled and state.throttle_until:
64
+ if now < state.throttle_until:
65
+ wait_seconds = (state.throttle_until - now).total_seconds()
66
+ return False, wait_seconds
67
+ else:
68
+ # Throttle period ended
69
+ state.is_throttled = False
70
+ state.throttle_until = None
71
+
72
+ # Clean old requests
73
+ minute_ago = now - timedelta(minutes=1)
74
+ hour_ago = now - timedelta(hours=1)
75
+
76
+ # Remove old requests from queues
77
+ while state.minute_requests and state.minute_requests[0] < minute_ago:
78
+ state.minute_requests.popleft()
79
+
80
+ while state.hour_requests and state.hour_requests[0] < hour_ago:
81
+ state.hour_requests.popleft()
82
+
83
+ # Check minute limit
84
+ if len(state.minute_requests) >= self.config.requests_per_minute:
85
+ # Calculate wait time
86
+ oldest = state.minute_requests[0]
87
+ wait_seconds = (oldest + timedelta(minutes=1) - now).total_seconds()
88
+ return False, max(0, wait_seconds)
89
+
90
+ # Check hour limit
91
+ if len(state.hour_requests) >= self.config.requests_per_hour:
92
+ # Calculate wait time
93
+ oldest = state.hour_requests[0]
94
+ wait_seconds = (oldest + timedelta(hours=1) - now).total_seconds()
95
+ return False, max(0, wait_seconds)
96
+
97
+ # Check burst limit
98
+ if state.last_request:
99
+ time_since_last = (now - state.last_request).total_seconds()
100
+ if time_since_last < 1.0 / self.config.burst_size:
101
+ wait_seconds = (1.0 / self.config.burst_size) - time_since_last
102
+ return False, wait_seconds
103
+
104
+ return True, 0
105
+
106
+ async def acquire(self, key: str = "default") -> bool:
107
+ """
108
+ Acquire a rate limit slot.
109
+ Waits if necessary.
110
+ """
111
+ while True:
112
+ allowed, wait_seconds = await self.check_rate_limit(key)
113
+
114
+ if allowed:
115
+ # Record request
116
+ state = self.get_state(key)
117
+ now = datetime.now()
118
+ state.minute_requests.append(now)
119
+ state.hour_requests.append(now)
120
+ state.last_request = now
121
+ state.total_requests += 1
122
+ return True
123
+
124
+ # Wait before retrying
125
+ if wait_seconds > 0:
126
+ await asyncio.sleep(min(wait_seconds, 5)) # Check every 5 seconds max
127
+
128
+ def record_error(self, key: str = "default", error: Exception = None):
129
+ """Record an error for the key."""
130
+ state = self.get_state(key)
131
+ state.consecutive_errors += 1
132
+ state.total_errors += 1
133
+
134
+ # Implement exponential backoff on errors
135
+ if state.consecutive_errors >= 3:
136
+ # Throttle for increasing periods
137
+ backoff_minutes = min(
138
+ self.config.backoff_base ** (state.consecutive_errors - 2),
139
+ 60 # Max 1 hour
140
+ )
141
+ state.is_throttled = True
142
+ state.throttle_until = datetime.now() + timedelta(minutes=backoff_minutes)
143
+
144
+ def record_success(self, key: str = "default"):
145
+ """Record a successful request."""
146
+ state = self.get_state(key)
147
+ state.consecutive_errors = 0
148
+
149
+ def get_status(self, key: str = "default") -> Dict[str, Any]:
150
+ """Get current status for monitoring."""
151
+ state = self.get_state(key)
152
+ now = datetime.now()
153
+
154
+ return {
155
+ "requests_last_minute": len(state.minute_requests),
156
+ "requests_last_hour": len(state.hour_requests),
157
+ "total_requests": state.total_requests,
158
+ "total_errors": state.total_errors,
159
+ "consecutive_errors": state.consecutive_errors,
160
+ "is_throttled": state.is_throttled,
161
+ "throttle_remaining": (
162
+ (state.throttle_until - now).total_seconds()
163
+ if state.throttle_until and now < state.throttle_until
164
+ else 0
165
+ ),
166
+ "minute_limit": self.config.requests_per_minute,
167
+ "hour_limit": self.config.requests_per_hour,
168
+ }
169
+
170
+
171
+ class ErrorRecovery:
172
+ """Error recovery with retries and fallback."""
173
+
174
+ def __init__(self, rate_limiter: RateLimiter = None):
175
+ """Initialize error recovery."""
176
+ self.rate_limiter = rate_limiter or RateLimiter()
177
+ self.fallback_handlers: Dict[type, Callable] = {}
178
+
179
+ def register_fallback(self, error_type: type, handler: Callable):
180
+ """Register a fallback handler for an error type."""
181
+ self.fallback_handlers[error_type] = handler
182
+
183
+ async def with_retry(
184
+ self,
185
+ func: Callable,
186
+ *args,
187
+ key: str = "default",
188
+ max_retries: Optional[int] = None,
189
+ **kwargs
190
+ ) -> Any:
191
+ """
192
+ Execute function with retry logic.
193
+ """
194
+ max_retries = max_retries or self.rate_limiter.config.max_retries
195
+ last_error = None
196
+
197
+ for attempt in range(max_retries):
198
+ try:
199
+ # Check rate limit
200
+ await self.rate_limiter.acquire(key)
201
+
202
+ # Execute function
203
+ result = await func(*args, **kwargs)
204
+
205
+ # Record success
206
+ self.rate_limiter.record_success(key)
207
+
208
+ return result
209
+
210
+ except Exception as e:
211
+ last_error = e
212
+ self.rate_limiter.record_error(key, e)
213
+
214
+ # Check for fallback handler
215
+ for error_type, handler in self.fallback_handlers.items():
216
+ if isinstance(e, error_type):
217
+ try:
218
+ return await handler(*args, **kwargs)
219
+ except:
220
+ pass # Fallback failed, continue with retry
221
+
222
+ # Calculate backoff
223
+ if attempt < max_retries - 1:
224
+ backoff = self.rate_limiter.config.backoff_base ** attempt
225
+
226
+ # Add jitter if configured
227
+ if self.rate_limiter.config.jitter:
228
+ backoff *= (0.5 + random.random())
229
+
230
+ await asyncio.sleep(min(backoff, 60)) # Max 60 seconds
231
+
232
+ # All retries failed
233
+ raise last_error or Exception("All retry attempts failed")
234
+
235
+ async def with_circuit_breaker(
236
+ self,
237
+ func: Callable,
238
+ *args,
239
+ key: str = "default",
240
+ threshold: int = 5,
241
+ timeout: int = 60,
242
+ **kwargs
243
+ ) -> Any:
244
+ """
245
+ Execute function with circuit breaker pattern.
246
+ """
247
+ state = self.rate_limiter.get_state(key)
248
+
249
+ # Check if circuit is open
250
+ if state.is_throttled:
251
+ raise Exception(f"Circuit breaker open for {key}")
252
+
253
+ try:
254
+ result = await self.with_retry(func, *args, key=key, **kwargs)
255
+ return result
256
+
257
+ except Exception as e:
258
+ # Check if we should open the circuit
259
+ if state.consecutive_errors >= threshold:
260
+ state.is_throttled = True
261
+ state.throttle_until = datetime.now() + timedelta(seconds=timeout)
262
+ raise Exception(f"Circuit breaker triggered for {key}: {e}")
263
+ raise
264
+
265
+
266
+ class SmartRateLimiter:
267
+ """Smart rate limiter that adapts to API responses."""
268
+
269
+ def __init__(self):
270
+ """Initialize smart rate limiter."""
271
+ self.limiters: Dict[str, RateLimiter] = {}
272
+ self.recovery = ErrorRecovery()
273
+
274
+ # Default configs for known APIs
275
+ self.configs = {
276
+ "openai": RateLimitConfig(
277
+ requests_per_minute=60,
278
+ requests_per_hour=1000,
279
+ burst_size=10
280
+ ),
281
+ "anthropic": RateLimitConfig(
282
+ requests_per_minute=50,
283
+ requests_per_hour=1000,
284
+ burst_size=5
285
+ ),
286
+ "local": RateLimitConfig(
287
+ requests_per_minute=100,
288
+ requests_per_hour=10000,
289
+ burst_size=20
290
+ ),
291
+ "free": RateLimitConfig(
292
+ requests_per_minute=10,
293
+ requests_per_hour=100,
294
+ burst_size=2
295
+ ),
296
+ }
297
+
298
+ def get_limiter(self, api_type: str) -> RateLimiter:
299
+ """Get or create limiter for API type."""
300
+ if api_type not in self.limiters:
301
+ config = self.configs.get(api_type, RateLimitConfig())
302
+ self.limiters[api_type] = RateLimiter(config)
303
+ return self.limiters[api_type]
304
+
305
+ async def execute_with_limit(
306
+ self,
307
+ api_type: str,
308
+ func: Callable,
309
+ *args,
310
+ **kwargs
311
+ ) -> Any:
312
+ """Execute function with appropriate rate limiting."""
313
+ limiter = self.get_limiter(api_type)
314
+ recovery = ErrorRecovery(limiter)
315
+
316
+ return await recovery.with_retry(
317
+ func,
318
+ *args,
319
+ key=api_type,
320
+ **kwargs
321
+ )
322
+
323
+ def get_all_status(self) -> Dict[str, Dict[str, Any]]:
324
+ """Get status of all limiters."""
325
+ return {
326
+ api_type: limiter.get_status()
327
+ for api_type, limiter in self.limiters.items()
328
+ }
329
+
330
+
331
+ # Global instance for easy use
332
+ smart_limiter = SmartRateLimiter()
@@ -0,0 +1,271 @@
1
+ """
2
+ Streaming response handler for Hanzo Dev.
3
+ Provides real-time feedback as AI generates responses.
4
+ """
5
+
6
+ import asyncio
7
+ from typing import AsyncGenerator, Optional, Callable
8
+ from rich.console import Console
9
+ from rich.live import Live
10
+ from rich.panel import Panel
11
+ from rich.markdown import Markdown
12
+ import time
13
+
14
+
15
+ class StreamingHandler:
16
+ """Handles streaming responses from AI models."""
17
+
18
+ def __init__(self, console: Console = None):
19
+ """Initialize streaming handler."""
20
+ self.console = console or Console()
21
+ self.current_response = ""
22
+ self.is_streaming = False
23
+
24
+ async def stream_openai(self, client, messages: list, model: str = "gpt-4") -> str:
25
+ """Stream response from OpenAI API."""
26
+ try:
27
+ stream = await client.chat.completions.create(
28
+ model=model,
29
+ messages=messages,
30
+ stream=True,
31
+ max_tokens=1000
32
+ )
33
+
34
+ self.current_response = ""
35
+ self.is_streaming = True
36
+
37
+ with Live(
38
+ Panel("", title="[bold cyan]AI Response[/bold cyan]",
39
+ title_align="left", border_style="dim cyan"),
40
+ console=self.console,
41
+ refresh_per_second=10
42
+ ) as live:
43
+ async for chunk in stream:
44
+ if chunk.choices[0].delta.content:
45
+ self.current_response += chunk.choices[0].delta.content
46
+ live.update(
47
+ Panel(
48
+ Markdown(self.current_response),
49
+ title="[bold cyan]AI Response[/bold cyan]",
50
+ title_align="left",
51
+ border_style="dim cyan",
52
+ padding=(1, 2)
53
+ )
54
+ )
55
+
56
+ self.is_streaming = False
57
+ return self.current_response
58
+
59
+ except Exception as e:
60
+ self.console.print(f"[red]Streaming error: {e}[/red]")
61
+ self.is_streaming = False
62
+ return None
63
+
64
+ async def stream_anthropic(self, client, messages: list, model: str = "claude-3-5-sonnet-20241022") -> str:
65
+ """Stream response from Anthropic API."""
66
+ try:
67
+ self.current_response = ""
68
+ self.is_streaming = True
69
+
70
+ with Live(
71
+ Panel("", title="[bold cyan]AI Response[/bold cyan]",
72
+ title_align="left", border_style="dim cyan"),
73
+ console=self.console,
74
+ refresh_per_second=10
75
+ ) as live:
76
+ async with client.messages.stream(
77
+ model=model,
78
+ messages=messages,
79
+ max_tokens=1000
80
+ ) as stream:
81
+ async for text in stream.text_stream:
82
+ self.current_response += text
83
+ live.update(
84
+ Panel(
85
+ Markdown(self.current_response),
86
+ title="[bold cyan]AI Response[/bold cyan]",
87
+ title_align="left",
88
+ border_style="dim cyan",
89
+ padding=(1, 2)
90
+ )
91
+ )
92
+
93
+ self.is_streaming = False
94
+ return self.current_response
95
+
96
+ except Exception as e:
97
+ self.console.print(f"[red]Streaming error: {e}[/red]")
98
+ self.is_streaming = False
99
+ return None
100
+
101
+ async def stream_ollama(self, message: str, model: str = "llama3.2") -> str:
102
+ """Stream response from Ollama local model."""
103
+ import httpx
104
+
105
+ try:
106
+ self.current_response = ""
107
+ self.is_streaming = True
108
+
109
+ with Live(
110
+ Panel("", title="[bold cyan]AI Response (Local)[/bold cyan]",
111
+ title_align="left", border_style="dim cyan"),
112
+ console=self.console,
113
+ refresh_per_second=10
114
+ ) as live:
115
+ async with httpx.AsyncClient() as client:
116
+ async with client.stream(
117
+ "POST",
118
+ "http://localhost:11434/api/generate",
119
+ json={"model": model, "prompt": message, "stream": True},
120
+ timeout=60.0
121
+ ) as response:
122
+ async for line in response.aiter_lines():
123
+ if line:
124
+ import json
125
+ data = json.loads(line)
126
+ if "response" in data:
127
+ self.current_response += data["response"]
128
+ live.update(
129
+ Panel(
130
+ Markdown(self.current_response),
131
+ title="[bold cyan]AI Response (Local)[/bold cyan]",
132
+ title_align="left",
133
+ border_style="dim cyan",
134
+ padding=(1, 2)
135
+ )
136
+ )
137
+ if data.get("done", False):
138
+ break
139
+
140
+ self.is_streaming = False
141
+ return self.current_response
142
+
143
+ except Exception as e:
144
+ self.console.print(f"[red]Ollama streaming error: {e}[/red]")
145
+ self.is_streaming = False
146
+ return None
147
+
148
+ async def simulate_streaming(self, text: str, delay: float = 0.02) -> str:
149
+ """Simulate streaming for non-streaming APIs."""
150
+ self.current_response = ""
151
+ self.is_streaming = True
152
+
153
+ words = text.split()
154
+
155
+ with Live(
156
+ Panel("", title="[bold cyan]AI Response[/bold cyan]",
157
+ title_align="left", border_style="dim cyan"),
158
+ console=self.console,
159
+ refresh_per_second=20
160
+ ) as live:
161
+ for i, word in enumerate(words):
162
+ self.current_response += word
163
+ if i < len(words) - 1:
164
+ self.current_response += " "
165
+
166
+ live.update(
167
+ Panel(
168
+ Markdown(self.current_response),
169
+ title="[bold cyan]AI Response[/bold cyan]",
170
+ title_align="left",
171
+ border_style="dim cyan",
172
+ padding=(1, 2)
173
+ )
174
+ )
175
+ await asyncio.sleep(delay)
176
+
177
+ self.is_streaming = False
178
+ return self.current_response
179
+
180
+ def stop_streaming(self):
181
+ """Stop current streaming operation."""
182
+ self.is_streaming = False
183
+ if self.current_response:
184
+ self.console.print(f"\n[yellow]Streaming interrupted[/yellow]")
185
+
186
+
187
+ class TypewriterEffect:
188
+ """Provides typewriter effect for text output."""
189
+
190
+ def __init__(self, console: Console = None):
191
+ self.console = console or Console()
192
+
193
+ async def type_text(self, text: str, speed: float = 0.03):
194
+ """Type text with typewriter effect."""
195
+ for char in text:
196
+ self.console.print(char, end="")
197
+ await asyncio.sleep(speed)
198
+ self.console.print() # New line at end
199
+
200
+ async def type_code(self, code: str, language: str = "python", speed: float = 0.01):
201
+ """Type code with syntax highlighting."""
202
+ from rich.syntax import Syntax
203
+
204
+ # Build up code progressively
205
+ current_code = ""
206
+ lines = code.split('\n')
207
+
208
+ with Live(console=self.console, refresh_per_second=30) as live:
209
+ for line in lines:
210
+ for char in line:
211
+ current_code += char
212
+ syntax = Syntax(current_code, language, theme="monokai", line_numbers=True)
213
+ live.update(syntax)
214
+ await asyncio.sleep(speed)
215
+ current_code += '\n'
216
+ syntax = Syntax(current_code, language, theme="monokai", line_numbers=True)
217
+ live.update(syntax)
218
+
219
+
220
+ async def stream_with_fallback(message: str, console: Console = None) -> Optional[str]:
221
+ """
222
+ Stream response with automatic fallback to available options.
223
+ """
224
+ import os
225
+ handler = StreamingHandler(console)
226
+
227
+ # Try OpenAI streaming
228
+ if os.getenv("OPENAI_API_KEY"):
229
+ try:
230
+ from openai import AsyncOpenAI
231
+ client = AsyncOpenAI()
232
+ return await handler.stream_openai(
233
+ client,
234
+ [{"role": "user", "content": message}]
235
+ )
236
+ except Exception as e:
237
+ if console:
238
+ console.print(f"[yellow]OpenAI streaming failed: {e}[/yellow]")
239
+
240
+ # Try Anthropic streaming
241
+ if os.getenv("ANTHROPIC_API_KEY"):
242
+ try:
243
+ from anthropic import AsyncAnthropic
244
+ client = AsyncAnthropic()
245
+ return await handler.stream_anthropic(
246
+ client,
247
+ [{"role": "user", "content": message}]
248
+ )
249
+ except Exception as e:
250
+ if console:
251
+ console.print(f"[yellow]Anthropic streaming failed: {e}[/yellow]")
252
+
253
+ # Try Ollama streaming
254
+ try:
255
+ return await handler.stream_ollama(message)
256
+ except:
257
+ pass
258
+
259
+ # Fallback to non-streaming with simulated effect
260
+ if console:
261
+ console.print("[yellow]Falling back to non-streaming mode[/yellow]")
262
+
263
+ # Get response from fallback handler
264
+ from .fallback_handler import smart_chat
265
+ response = await smart_chat(message, console)
266
+
267
+ if response:
268
+ # Simulate streaming
269
+ return await handler.simulate_streaming(response)
270
+
271
+ return None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes