ralphx 0.3.5__py3-none-any.whl → 0.4.0__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.
- ralphx/__init__.py +1 -1
- ralphx/adapters/base.py +10 -2
- ralphx/adapters/claude_cli.py +222 -82
- ralphx/api/routes/auth.py +105 -32
- ralphx/api/routes/planning.py +865 -16
- ralphx/api/routes/resources.py +528 -6
- ralphx/api/routes/stream.py +58 -56
- ralphx/api/routes/workflows.py +257 -25
- ralphx/core/auth.py +32 -7
- ralphx/core/executor.py +170 -16
- ralphx/core/loop_templates.py +26 -0
- ralphx/core/planning_iteration_executor.py +633 -0
- ralphx/core/planning_service.py +10 -3
- ralphx/core/project_db.py +770 -79
- ralphx/core/resources.py +28 -2
- ralphx/core/workflow_executor.py +32 -3
- ralphx/mcp/tools/diagnostics.py +1 -1
- ralphx/mcp/tools/monitoring.py +10 -16
- ralphx/mcp/tools/workflows.py +3 -3
- ralphx/static/assets/index-BuLI7ffn.css +1 -0
- ralphx/static/assets/index-DWvlqOTb.js +264 -0
- ralphx/static/assets/index-DWvlqOTb.js.map +1 -0
- ralphx/static/index.html +2 -2
- ralphx/templates/loop_templates/consumer.md +2 -2
- {ralphx-0.3.5.dist-info → ralphx-0.4.0.dist-info}/METADATA +1 -1
- {ralphx-0.3.5.dist-info → ralphx-0.4.0.dist-info}/RECORD +28 -27
- ralphx/static/assets/index-0ovNnfOq.css +0 -1
- ralphx/static/assets/index-CY9s08ZB.js +0 -251
- ralphx/static/assets/index-CY9s08ZB.js.map +0 -1
- {ralphx-0.3.5.dist-info → ralphx-0.4.0.dist-info}/WHEEL +0 -0
- {ralphx-0.3.5.dist-info → ralphx-0.4.0.dist-info}/entry_points.txt +0 -0
ralphx/api/routes/auth.py
CHANGED
|
@@ -93,6 +93,53 @@ class AssignAccountRequest(BaseModel):
|
|
|
93
93
|
# ============================================================================
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
async def _fetch_account_profile(access_token: str) -> Optional[dict]:
|
|
97
|
+
"""Fetch account profile from Anthropic API.
|
|
98
|
+
|
|
99
|
+
Returns subscription type, rate limit tier, and other account info.
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
async with httpx.AsyncClient() as client:
|
|
103
|
+
response = await client.get(
|
|
104
|
+
"https://api.anthropic.com/api/oauth/profile",
|
|
105
|
+
headers={
|
|
106
|
+
"Authorization": f"Bearer {access_token}",
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
109
|
+
},
|
|
110
|
+
timeout=10.0,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if response.status_code != 200:
|
|
114
|
+
logger.warning(f"Profile fetch failed: {response.status_code}")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
data = response.json()
|
|
118
|
+
org = data.get("organization", {})
|
|
119
|
+
account = data.get("account", {})
|
|
120
|
+
|
|
121
|
+
# Map organization_type to subscription type
|
|
122
|
+
org_type = org.get("organization_type")
|
|
123
|
+
subscription_map = {
|
|
124
|
+
"claude_max": "max",
|
|
125
|
+
"claude_pro": "pro",
|
|
126
|
+
"claude_enterprise": "enterprise",
|
|
127
|
+
"claude_team": "team",
|
|
128
|
+
}
|
|
129
|
+
subscription_type = subscription_map.get(org_type)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"subscription_type": subscription_type,
|
|
133
|
+
"rate_limit_tier": org.get("rate_limit_tier"),
|
|
134
|
+
"has_extra_usage_enabled": org.get("has_extra_usage_enabled"),
|
|
135
|
+
"display_name": account.get("display_name"),
|
|
136
|
+
"full_name": account.get("full_name"),
|
|
137
|
+
}
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning(f"Profile fetch error: {e}")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
96
143
|
async def _fetch_account_usage(access_token: str) -> Optional[dict]:
|
|
97
144
|
"""Fetch usage data from Anthropic API."""
|
|
98
145
|
try:
|
|
@@ -112,6 +159,7 @@ async def _fetch_account_usage(access_token: str) -> Optional[dict]:
|
|
|
112
159
|
data = response.json()
|
|
113
160
|
five_hour = data.get("five_hour", {})
|
|
114
161
|
seven_day = data.get("seven_day", {})
|
|
162
|
+
|
|
115
163
|
return {
|
|
116
164
|
"five_hour": five_hour.get("utilization", 0),
|
|
117
165
|
"seven_day": seven_day.get("utilization", 0),
|
|
@@ -184,6 +232,7 @@ from ralphx.core.auth import (
|
|
|
184
232
|
AuthStatus,
|
|
185
233
|
CLIENT_ID,
|
|
186
234
|
TOKEN_URL,
|
|
235
|
+
_token_refresh_lock,
|
|
187
236
|
get_auth_status,
|
|
188
237
|
refresh_token_if_needed,
|
|
189
238
|
force_refresh_token,
|
|
@@ -424,9 +473,20 @@ async def add_account(expected_email: Optional[str] = None):
|
|
|
424
473
|
|
|
425
474
|
# Use store_oauth_tokens to properly serialize scopes to JSON
|
|
426
475
|
account = store_oauth_tokens(tokens)
|
|
427
|
-
|
|
428
|
-
# Fetch usage data immediately
|
|
429
476
|
db = Database()
|
|
477
|
+
|
|
478
|
+
# Fetch profile to get subscription type
|
|
479
|
+
profile_data = await _fetch_account_profile(tokens["access_token"])
|
|
480
|
+
if profile_data:
|
|
481
|
+
update_fields = {}
|
|
482
|
+
if profile_data.get("subscription_type"):
|
|
483
|
+
update_fields["subscription_type"] = profile_data["subscription_type"]
|
|
484
|
+
if profile_data.get("rate_limit_tier"):
|
|
485
|
+
update_fields["rate_limit_tier"] = profile_data["rate_limit_tier"]
|
|
486
|
+
if update_fields:
|
|
487
|
+
db.update_account(account["id"], **update_fields)
|
|
488
|
+
|
|
489
|
+
# Fetch usage data
|
|
430
490
|
usage_data = await _fetch_account_usage(tokens["access_token"])
|
|
431
491
|
if usage_data:
|
|
432
492
|
db.update_account_usage_cache(
|
|
@@ -565,45 +625,58 @@ async def refresh_account_token(account_id: int):
|
|
|
565
625
|
return {"success": False, "message": "No refresh token available"}
|
|
566
626
|
|
|
567
627
|
try:
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
628
|
+
# Use token refresh lock to prevent race conditions with background
|
|
629
|
+
# refresh tasks or concurrent manual refresh requests
|
|
630
|
+
async with _token_refresh_lock():
|
|
631
|
+
# Re-fetch account inside lock to get latest refresh_token
|
|
632
|
+
# (another process may have refreshed while we waited for lock)
|
|
633
|
+
account = db.get_account(account_id)
|
|
634
|
+
if not account or not account.get("refresh_token"):
|
|
635
|
+
return {"success": False, "message": "No refresh token available"}
|
|
636
|
+
|
|
637
|
+
async with httpx.AsyncClient() as client:
|
|
638
|
+
resp = await client.post(
|
|
639
|
+
TOKEN_URL,
|
|
640
|
+
json={
|
|
641
|
+
"grant_type": "refresh_token",
|
|
642
|
+
"refresh_token": account["refresh_token"],
|
|
643
|
+
"client_id": CLIENT_ID,
|
|
644
|
+
},
|
|
645
|
+
headers={
|
|
646
|
+
"Content-Type": "application/json",
|
|
647
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
648
|
+
},
|
|
649
|
+
)
|
|
581
650
|
|
|
582
|
-
|
|
583
|
-
|
|
651
|
+
if resp.status_code != 200:
|
|
652
|
+
return {"success": False, "message": f"Token refresh failed: {resp.status_code}"}
|
|
584
653
|
|
|
585
|
-
|
|
586
|
-
|
|
654
|
+
tokens = resp.json()
|
|
655
|
+
new_expires_at = int(time.time()) + tokens.get("expires_in", 28800)
|
|
587
656
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
657
|
+
# Build update dict with all available fields
|
|
658
|
+
update_data = {
|
|
659
|
+
"access_token": tokens["access_token"],
|
|
660
|
+
"refresh_token": tokens.get("refresh_token", account["refresh_token"]),
|
|
661
|
+
"expires_at": new_expires_at,
|
|
662
|
+
}
|
|
594
663
|
|
|
595
|
-
|
|
596
|
-
if tokens.get("subscription_type"):
|
|
597
|
-
update_data["subscription_type"] = tokens["subscription_type"]
|
|
598
|
-
if tokens.get("rate_limit_tier"):
|
|
599
|
-
update_data["rate_limit_tier"] = tokens["rate_limit_tier"]
|
|
664
|
+
db.update_account(account_id, **update_data)
|
|
600
665
|
|
|
601
|
-
|
|
666
|
+
# Fetch profile outside the lock (uses the new access_token, not refresh_token)
|
|
667
|
+
profile_data = await _fetch_account_profile(tokens["access_token"])
|
|
668
|
+
subscription_type = None
|
|
669
|
+
if profile_data:
|
|
670
|
+
if profile_data.get("subscription_type"):
|
|
671
|
+
subscription_type = profile_data["subscription_type"]
|
|
672
|
+
db.update_account(account_id, subscription_type=subscription_type)
|
|
673
|
+
if profile_data.get("rate_limit_tier"):
|
|
674
|
+
db.update_account(account_id, rate_limit_tier=profile_data["rate_limit_tier"])
|
|
602
675
|
|
|
603
676
|
return {
|
|
604
677
|
"success": True,
|
|
605
678
|
"expires_at": new_expires_at,
|
|
606
|
-
"subscription_type":
|
|
679
|
+
"subscription_type": subscription_type,
|
|
607
680
|
}
|
|
608
681
|
except Exception as e:
|
|
609
682
|
return {"success": False, "message": str(e)}
|