feat: enhance billing dashboard with historical data & security hardening
- Fix usage tracking zero-value bug with self-healing logic - Add month selector for historical usage views - Implement start-of-month graceful initialization - Merge PR #372: Harden user-scoped access in subscription routes - Fix UI bugs in UsageDashboard component
This commit is contained in:
@@ -555,7 +555,10 @@ async def get_agent_huddle_feed_endpoint(
|
|||||||
try:
|
try:
|
||||||
user_id = str(current_user.get("id"))
|
user_id = str(current_user.get("id"))
|
||||||
service = AgentActivityService(db, user_id)
|
service = AgentActivityService(db, user_id)
|
||||||
feed = service.get_huddle_feed(
|
|
||||||
|
# Use run_in_threadpool to execute the blocking service call in a separate thread
|
||||||
|
feed = await run_in_threadpool(
|
||||||
|
service.get_huddle_feed,
|
||||||
since=since,
|
since=since,
|
||||||
cursor=cursor,
|
cursor=cursor,
|
||||||
runs_limit=runs_limit,
|
runs_limit=runs_limit,
|
||||||
|
|||||||
@@ -48,15 +48,19 @@ async def get_today_workflow(
|
|||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
user_id = str(current_user.get("id"))
|
user_id = str(current_user.get("id"))
|
||||||
plan, created = await get_or_create_daily_workflow_plan(db, user_id, date=date)
|
plan, created = await get_or_create_daily_workflow_plan(db, user_id, date=date)
|
||||||
|
|
||||||
tasks = (
|
def _fetch_tasks():
|
||||||
db.query(DailyWorkflowTask)
|
return (
|
||||||
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
|
db.query(DailyWorkflowTask)
|
||||||
.order_by(DailyWorkflowTask.created_at.asc())
|
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
|
||||||
.all()
|
.order_by(DailyWorkflowTask.created_at.asc())
|
||||||
)
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
tasks = await run_in_threadpool(_fetch_tasks)
|
||||||
|
|
||||||
response_tasks = []
|
response_tasks = []
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
@@ -100,18 +104,26 @@ async def get_today_workflow(
|
|||||||
from datetime import date as date_type, timedelta
|
from datetime import date as date_type, timedelta
|
||||||
|
|
||||||
y_str = (date_type.fromisoformat(plan.date) - timedelta(days=1)).isoformat()
|
y_str = (date_type.fromisoformat(plan.date) - timedelta(days=1)).isoformat()
|
||||||
y_plan = (
|
|
||||||
db.query(DailyWorkflowPlan)
|
def _fetch_yesterday():
|
||||||
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == y_str)
|
y_plan = (
|
||||||
.first()
|
db.query(DailyWorkflowPlan)
|
||||||
)
|
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == y_str)
|
||||||
if y_plan:
|
.first()
|
||||||
y_tasks = (
|
|
||||||
db.query(DailyWorkflowTask)
|
|
||||||
.filter(DailyWorkflowTask.plan_id == y_plan.id, DailyWorkflowTask.user_id == user_id)
|
|
||||||
.order_by(DailyWorkflowTask.created_at.asc())
|
|
||||||
.all()
|
|
||||||
)
|
)
|
||||||
|
if y_plan:
|
||||||
|
y_tasks = (
|
||||||
|
db.query(DailyWorkflowTask)
|
||||||
|
.filter(DailyWorkflowTask.plan_id == y_plan.id, DailyWorkflowTask.user_id == user_id)
|
||||||
|
.order_by(DailyWorkflowTask.created_at.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return y_tasks
|
||||||
|
return []
|
||||||
|
|
||||||
|
y_tasks = await run_in_threadpool(_fetch_yesterday)
|
||||||
|
|
||||||
|
if y_tasks:
|
||||||
y_response = []
|
y_response = []
|
||||||
for t in y_tasks:
|
for t in y_tasks:
|
||||||
y_response.append(
|
y_response.append(
|
||||||
|
|||||||
@@ -158,25 +158,25 @@ def huggingface_text_response(
|
|||||||
if not api_key:
|
if not api_key:
|
||||||
raise Exception("HF_TOKEN not found in environment variables")
|
raise Exception("HF_TOKEN not found in environment variables")
|
||||||
|
|
||||||
# Initialize Hugging Face client using Responses API
|
# Initialize Hugging Face client
|
||||||
client = OpenAI(
|
client = OpenAI(
|
||||||
base_url="https://router.huggingface.co/v1",
|
base_url=f"https://router.huggingface.co/hf/v1",
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
logger.info("✅ Hugging Face client initialized for text response")
|
logger.info("✅ Hugging Face client initialized for text response")
|
||||||
|
|
||||||
# Prepare input for the API
|
# Prepare input for the API
|
||||||
input_content = []
|
messages = []
|
||||||
|
|
||||||
# Add system prompt if provided
|
# Add system prompt if provided
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
input_content.append({
|
messages.append({
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": system_prompt
|
"content": system_prompt
|
||||||
})
|
})
|
||||||
|
|
||||||
# Add user prompt
|
# Add user prompt
|
||||||
input_content.append({
|
messages.append({
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": prompt
|
"content": prompt
|
||||||
})
|
})
|
||||||
@@ -191,31 +191,23 @@ def huggingface_text_response(
|
|||||||
max_tokens,
|
max_tokens,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("🚀 Making Hugging Face API call...")
|
logger.info("🚀 Making Hugging Face API call (chat completion)...")
|
||||||
|
|
||||||
# Add rate limiting to prevent expensive API calls
|
# Add rate limiting to prevent expensive API calls
|
||||||
import time
|
import time
|
||||||
time.sleep(1) # 1 second delay between API calls
|
time.sleep(1) # 1 second delay between API calls
|
||||||
|
|
||||||
# Make the API call using Responses API
|
# Make the API call using Chat Completions
|
||||||
response = client.responses.parse(
|
response = client.chat.completions.create(
|
||||||
model=model,
|
model=model,
|
||||||
input=input_content,
|
messages=messages,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
top_p=top_p,
|
top_p=top_p,
|
||||||
|
max_tokens=max_tokens
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract text from response
|
# Extract text from response
|
||||||
if hasattr(response, 'output_text') and response.output_text:
|
generated_text = response.choices[0].message.content
|
||||||
generated_text = response.output_text
|
|
||||||
elif hasattr(response, 'output') and response.output:
|
|
||||||
# Handle case where output is a list
|
|
||||||
if isinstance(response.output, list) and len(response.output) > 0:
|
|
||||||
generated_text = response.output[0].get('content', '')
|
|
||||||
else:
|
|
||||||
generated_text = str(response.output)
|
|
||||||
else:
|
|
||||||
generated_text = str(response)
|
|
||||||
|
|
||||||
# Clean up the response
|
# Clean up the response
|
||||||
if generated_text:
|
if generated_text:
|
||||||
@@ -296,26 +288,28 @@ def huggingface_structured_json_response(
|
|||||||
if not api_key:
|
if not api_key:
|
||||||
raise Exception("HF_TOKEN not found in environment variables")
|
raise Exception("HF_TOKEN not found in environment variables")
|
||||||
|
|
||||||
# Initialize Hugging Face client using Responses API
|
# Initialize OpenAI client with Hugging Face base URL
|
||||||
|
# Use standard Inference API endpoint
|
||||||
client = OpenAI(
|
client = OpenAI(
|
||||||
base_url="https://router.huggingface.co/v1",
|
base_url=f"https://router.huggingface.co/hf/v1",
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
logger.info("✅ Hugging Face client initialized for structured JSON response")
|
logger.info("✅ Hugging Face client initialized for structured JSON response")
|
||||||
|
|
||||||
# Prepare input for the API
|
# Prepare input for the API
|
||||||
input_content = []
|
messages = []
|
||||||
|
|
||||||
# Add system prompt if provided
|
# Add system prompt if provided
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
input_content.append({
|
messages.append({
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": system_prompt
|
"content": system_prompt
|
||||||
})
|
})
|
||||||
|
|
||||||
# Add user prompt with JSON instruction
|
# Add user prompt with JSON instruction
|
||||||
|
# For HF models, explicit JSON instruction in prompt is often better than response_format
|
||||||
json_instruction = "Please respond with valid JSON that matches the provided schema."
|
json_instruction = "Please respond with valid JSON that matches the provided schema."
|
||||||
input_content.append({
|
messages.append({
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": f"{prompt}\n\n{json_instruction}"
|
"content": f"{prompt}\n\n{json_instruction}"
|
||||||
})
|
})
|
||||||
@@ -332,52 +326,39 @@ def huggingface_structured_json_response(
|
|||||||
|
|
||||||
logger.info("🚀 Making Hugging Face structured API call...")
|
logger.info("🚀 Making Hugging Face structured API call...")
|
||||||
|
|
||||||
# Make the API call using Responses API with structured output
|
# Make the API call using standard Chat Completions
|
||||||
# Use simple text generation and parse JSON manually to avoid API format issues
|
logger.info("🚀 Making Hugging Face API call (chat completion)...")
|
||||||
logger.info("🚀 Making Hugging Face API call (text mode with JSON parsing)...")
|
|
||||||
|
|
||||||
# Add JSON instruction to the prompt
|
# Add JSON schema to prompt for guidance
|
||||||
json_instruction = "\n\nPlease respond with valid JSON that matches this exact structure:\n" + json.dumps(schema, indent=2)
|
json_schema_str = json.dumps(schema, indent=2)
|
||||||
input_content[-1]["content"] = input_content[-1]["content"] + json_instruction
|
messages[-1]["content"] += f"\n\nJSON Schema:\n{json_schema_str}"
|
||||||
|
|
||||||
# Add rate limiting to prevent expensive API calls
|
# Add rate limiting to prevent expensive API calls
|
||||||
import time
|
import time
|
||||||
time.sleep(1) # 1 second delay between API calls
|
time.sleep(1) # 1 second delay between API calls
|
||||||
|
|
||||||
response = client.responses.parse(
|
try:
|
||||||
model=model,
|
response = client.chat.completions.create(
|
||||||
input=input_content,
|
model=model,
|
||||||
temperature=temperature
|
messages=messages,
|
||||||
)
|
temperature=temperature,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
response_format={"type": "json_object"} # Try to enforce JSON mode if supported
|
||||||
|
)
|
||||||
|
|
||||||
# Extract structured data from response
|
response_text = response.choices[0].message.content
|
||||||
if hasattr(response, 'output_parsed') and response.output_parsed:
|
|
||||||
# The new API returns parsed data directly (Pydantic model case)
|
# Clean up response text if needed
|
||||||
logger.info("✅ Hugging Face structured JSON response parsed successfully")
|
response_text = response_text.strip()
|
||||||
# Convert Pydantic model to dict if needed
|
if response_text.startswith("```json"):
|
||||||
if hasattr(response.output_parsed, 'model_dump'):
|
response_text = response_text[7:]
|
||||||
return response.output_parsed.model_dump()
|
if response_text.endswith("```"):
|
||||||
elif hasattr(response.output_parsed, 'dict'):
|
response_text = response_text[:-3]
|
||||||
return response.output_parsed.dict()
|
|
||||||
else:
|
|
||||||
return response.output_parsed
|
|
||||||
elif hasattr(response, 'output_text') and response.output_text:
|
|
||||||
# Fallback to text parsing if output_parsed is not available
|
|
||||||
response_text = response.output_text
|
|
||||||
# Clean up the response text
|
|
||||||
response_text = re.sub(r'```json\n?', '', response_text)
|
|
||||||
response_text = re.sub(r'```\n?', '', response_text)
|
|
||||||
response_text = response_text.strip()
|
response_text = response_text.strip()
|
||||||
|
|
||||||
# Fix common markdown artefacts that break JSON, e.g. lines starting with **"key":
|
|
||||||
# **"narration": "text"
|
|
||||||
# becomes:
|
|
||||||
# "narration": "text"
|
|
||||||
response_text = re.sub(r'^\s*\*\*(?=\s*")', '', response_text, flags=re.MULTILINE)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parsed_json = json.loads(response_text)
|
parsed_json = json.loads(response_text)
|
||||||
logger.info("✅ Hugging Face structured JSON response parsed from text")
|
logger.info("✅ Hugging Face structured JSON response parsed successfully")
|
||||||
return parsed_json
|
return parsed_json
|
||||||
except json.JSONDecodeError as json_err:
|
except json.JSONDecodeError as json_err:
|
||||||
logger.error(f"❌ JSON parsing failed: {json_err}")
|
logger.error(f"❌ JSON parsing failed: {json_err}")
|
||||||
@@ -393,20 +374,30 @@ def huggingface_structured_json_response(
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# If all else fails, return a structured error response
|
return {"error": "Failed to parse JSON response", "raw_response": response_text}
|
||||||
logger.error("❌ All JSON parsing attempts failed")
|
|
||||||
return {
|
except Exception as e:
|
||||||
"error": "Failed to parse JSON response",
|
logger.error(f"❌ Hugging Face API call failed: {e}")
|
||||||
"raw_response": response_text,
|
# If 422 Unprocessable Entity (often due to response_format not supported), retry without it
|
||||||
"schema_expected": schema
|
if "422" in str(e) or "not supported" in str(e).lower():
|
||||||
}
|
logger.info("Retrying without response_format...")
|
||||||
else:
|
response = client.chat.completions.create(
|
||||||
logger.error("❌ No valid response data found")
|
model=model,
|
||||||
return {
|
messages=messages,
|
||||||
"error": "No valid response data found",
|
temperature=temperature,
|
||||||
"raw_response": str(response),
|
max_tokens=max_tokens
|
||||||
"schema_expected": schema
|
)
|
||||||
}
|
response_text = response.choices[0].message.content
|
||||||
|
# ... (same parsing logic would apply, simplified here for brevity)
|
||||||
|
try:
|
||||||
|
return json.loads(response_text)
|
||||||
|
except:
|
||||||
|
# Regex fallback
|
||||||
|
json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
return json.loads(json_match.group())
|
||||||
|
return {"error": "Failed to parse JSON response", "raw_response": response_text}
|
||||||
|
raise e
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e) if str(e) else repr(e)
|
error_msg = str(e) if str(e) else repr(e)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
|||||||
model = "gemini-2.0-flash-001"
|
model = "gemini-2.0-flash-001"
|
||||||
elif env_provider in ['hf_response_api', 'huggingface', 'hf']:
|
elif env_provider in ['hf_response_api', 'huggingface', 'hf']:
|
||||||
gpt_provider = "huggingface"
|
gpt_provider = "huggingface"
|
||||||
model = "openai/gpt-oss-120b:groq"
|
model = "mistralai/Mistral-7B-Instruct-v0.3"
|
||||||
|
|
||||||
# Default blog characteristics
|
# Default blog characteristics
|
||||||
blog_tone = "Professional"
|
blog_tone = "Professional"
|
||||||
@@ -80,7 +80,7 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
|||||||
model = "gemini-2.0-flash-001"
|
model = "gemini-2.0-flash-001"
|
||||||
elif "huggingface" in available_providers:
|
elif "huggingface" in available_providers:
|
||||||
gpt_provider = "huggingface"
|
gpt_provider = "huggingface"
|
||||||
model = "openai/gpt-oss-120b:groq"
|
model = "mistralai/Mistral-7B-Instruct-v0.3"
|
||||||
else:
|
else:
|
||||||
logger.error("[llm_text_gen] No API keys found for supported providers.")
|
logger.error("[llm_text_gen] No API keys found for supported providers.")
|
||||||
raise RuntimeError("No LLM API keys configured. Configure GEMINI_API_KEY or HF_TOKEN to enable AI responses.")
|
raise RuntimeError("No LLM API keys configured. Configure GEMINI_API_KEY or HF_TOKEN to enable AI responses.")
|
||||||
@@ -93,7 +93,7 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
|||||||
model = "gemini-2.0-flash-001"
|
model = "gemini-2.0-flash-001"
|
||||||
elif "huggingface" in available_providers:
|
elif "huggingface" in available_providers:
|
||||||
gpt_provider = "huggingface"
|
gpt_provider = "huggingface"
|
||||||
model = "openai/gpt-oss-120b:groq"
|
model = "mistralai/Mistral-7B-Instruct-v0.3"
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("No supported providers available.")
|
raise RuntimeError("No supported providers available.")
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
|||||||
elif fallback_provider == "huggingface":
|
elif fallback_provider == "huggingface":
|
||||||
provider_enum = APIProvider.MISTRAL
|
provider_enum = APIProvider.MISTRAL
|
||||||
actual_provider_name = "huggingface"
|
actual_provider_name = "huggingface"
|
||||||
fallback_model = "openai/gpt-oss-120b:groq"
|
fallback_model = "mistralai/Mistral-7B-Instruct-v0.3"
|
||||||
|
|
||||||
if fallback_provider == "google":
|
if fallback_provider == "google":
|
||||||
if json_struct:
|
if json_struct:
|
||||||
@@ -330,7 +330,7 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
|||||||
response_text = huggingface_structured_json_response(
|
response_text = huggingface_structured_json_response(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
schema=json_struct,
|
schema=json_struct,
|
||||||
model="openai/gpt-oss-120b:groq",
|
model="mistralai/Mistral-7B-Instruct-v0.3",
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
system_prompt=system_instructions
|
system_prompt=system_instructions
|
||||||
@@ -338,7 +338,7 @@ def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct:
|
|||||||
else:
|
else:
|
||||||
response_text = huggingface_text_response(
|
response_text = huggingface_text_response(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
model="openai/gpt-oss-120b:groq",
|
model="mistralai/Mistral-7B-Instruct-v0.3",
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
top_p=top_p,
|
top_p=top_p,
|
||||||
|
|||||||
@@ -477,14 +477,20 @@ async def get_lightweight_stats(user_id: str) -> Dict[str, Any]:
|
|||||||
|
|
||||||
# Optimized: Single query with conditional aggregation instead of two separate queries
|
# Optimized: Single query with conditional aggregation instead of two separate queries
|
||||||
# This is much faster as it only scans the table once
|
# This is much faster as it only scans the table once
|
||||||
stats = db.query(
|
# Use run_in_threadpool to avoid blocking the event loop with sync DB query
|
||||||
func.count(APIRequest.id).label('total_requests'),
|
from starlette.concurrency import run_in_threadpool
|
||||||
func.sum(
|
|
||||||
case((APIRequest.status_code >= 400, 1), else_=0)
|
def _fetch_stats():
|
||||||
).label('total_errors')
|
return db.query(
|
||||||
).filter(
|
func.count(APIRequest.id).label('total_requests'),
|
||||||
APIRequest.timestamp >= five_minutes_ago
|
func.sum(
|
||||||
).first()
|
case((APIRequest.status_code >= 400, 1), else_=0)
|
||||||
|
).label('total_errors')
|
||||||
|
).filter(
|
||||||
|
APIRequest.timestamp >= five_minutes_ago
|
||||||
|
).first()
|
||||||
|
|
||||||
|
stats = await run_in_threadpool(_fetch_stats)
|
||||||
|
|
||||||
recent_requests = stats.total_requests or 0 if stats else 0
|
recent_requests = stats.total_requests or 0 if stats else 0
|
||||||
recent_errors = int(stats.total_errors or 0) if stats else 0
|
recent_errors = int(stats.total_errors or 0) if stats else 0
|
||||||
|
|||||||
@@ -476,54 +476,65 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
|
|||||||
|
|
||||||
|
|
||||||
async def get_or_create_daily_workflow_plan(db: Session, user_id: str, date: Optional[str] = None) -> tuple[DailyWorkflowPlan, bool]:
|
async def get_or_create_daily_workflow_plan(db: Session, user_id: str, date: Optional[str] = None) -> tuple[DailyWorkflowPlan, bool]:
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
date_str = date or _today_date_str()
|
date_str = date or _today_date_str()
|
||||||
existing = (
|
|
||||||
db.query(DailyWorkflowPlan)
|
def _get_existing():
|
||||||
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == date_str)
|
return (
|
||||||
.first()
|
db.query(DailyWorkflowPlan)
|
||||||
)
|
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == date_str)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
existing = await run_in_threadpool(_get_existing)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
return existing, False
|
return existing, False
|
||||||
|
|
||||||
plan_data = await generate_agent_enhanced_plan(db, user_id, date_str)
|
plan_data = await generate_agent_enhanced_plan(db, user_id, date_str)
|
||||||
tasks = plan_data.get("tasks", [])
|
tasks = plan_data.get("tasks", [])
|
||||||
|
|
||||||
plan = DailyWorkflowPlan(
|
def _create_plan():
|
||||||
user_id=user_id,
|
plan = DailyWorkflowPlan(
|
||||||
date=date_str,
|
|
||||||
source="agent",
|
|
||||||
plan_json=plan_data,
|
|
||||||
created_at=datetime.utcnow(),
|
|
||||||
updated_at=datetime.utcnow(),
|
|
||||||
)
|
|
||||||
db.add(plan)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(plan)
|
|
||||||
|
|
||||||
for t in tasks:
|
|
||||||
pillar_id = str(t.get("pillarId") or "").lower().strip()
|
|
||||||
if pillar_id not in PILLAR_IDS:
|
|
||||||
continue
|
|
||||||
task = DailyWorkflowTask(
|
|
||||||
plan_id=plan.id,
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
pillar_id=pillar_id,
|
date=date_str,
|
||||||
title=str(t.get("title") or "Task").strip()[:255],
|
source="agent",
|
||||||
description=str(t.get("description") or "").strip(),
|
plan_json=plan_data,
|
||||||
status=_coerce_status(t.get("status")),
|
|
||||||
priority=_coerce_priority(t.get("priority")),
|
|
||||||
estimated_time=int(t.get("estimatedTime") or 15),
|
|
||||||
action_type=str(t.get("actionType") or "navigate").strip()[:20],
|
|
||||||
action_url=str(t.get("actionUrl") or "").strip() or None,
|
|
||||||
enabled=bool(t.get("enabled", True)),
|
|
||||||
dependencies=t.get("dependencies") if isinstance(t.get("dependencies"), list) else None,
|
|
||||||
metadata_json=t.get("metadata") if isinstance(t.get("metadata"), dict) else None,
|
|
||||||
created_at=datetime.utcnow(),
|
created_at=datetime.utcnow(),
|
||||||
updated_at=datetime.utcnow(),
|
updated_at=datetime.utcnow(),
|
||||||
)
|
)
|
||||||
db.add(task)
|
db.add(plan)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(plan)
|
db.refresh(plan)
|
||||||
|
|
||||||
|
for t in tasks:
|
||||||
|
pillar_id = str(t.get("pillarId") or "").lower().strip()
|
||||||
|
if pillar_id not in PILLAR_IDS:
|
||||||
|
continue
|
||||||
|
task = DailyWorkflowTask(
|
||||||
|
plan_id=plan.id,
|
||||||
|
user_id=user_id,
|
||||||
|
pillar_id=pillar_id,
|
||||||
|
title=str(t.get("title") or "Task").strip()[:255],
|
||||||
|
description=str(t.get("description") or "").strip(),
|
||||||
|
status=_coerce_status(t.get("status")),
|
||||||
|
priority=_coerce_priority(t.get("priority")),
|
||||||
|
estimated_time=int(t.get("estimatedTime") or 15),
|
||||||
|
action_type=str(t.get("actionType") or "navigate").strip()[:20],
|
||||||
|
action_url=str(t.get("actionUrl") or "").strip(),
|
||||||
|
dependencies=json.dumps(t.get("dependencies") or []),
|
||||||
|
metadata_json=t.get("metadata") or {},
|
||||||
|
enabled=bool(t.get("enabled", True)),
|
||||||
|
created_at=datetime.utcnow(),
|
||||||
|
updated_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
db.add(task)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return plan
|
||||||
|
|
||||||
|
plan = await run_in_threadpool(_create_plan)
|
||||||
return plan, True
|
return plan, True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,13 +61,16 @@ const EnhancedStrategyActivationButton: React.FC<EnhancedStrategyActivationButto
|
|||||||
|
|
||||||
const handleSetupMonitoring = async (monitoringPlan: any) => {
|
const handleSetupMonitoring = async (monitoringPlan: any) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// try {
|
setActivationProgress(10);
|
||||||
|
|
||||||
|
try {
|
||||||
console.log('🎯 EnhancedStrategyActivationButton: handleSetupMonitoring called');
|
console.log('🎯 EnhancedStrategyActivationButton: handleSetupMonitoring called');
|
||||||
|
|
||||||
// Get strategy ID
|
// Get strategy ID
|
||||||
const strategyId = strategyData?.id || 1;
|
const strategyId = strategyData?.id || 1;
|
||||||
|
|
||||||
// Step 1: Generate monitoring plan if not provided
|
// Step 1: Generate monitoring plan if not provided
|
||||||
|
setActivationProgress(30);
|
||||||
let finalMonitoringPlan = monitoringPlan;
|
let finalMonitoringPlan = monitoringPlan;
|
||||||
if (!finalMonitoringPlan) {
|
if (!finalMonitoringPlan) {
|
||||||
console.log('🎯 Generating monitoring plan...');
|
console.log('🎯 Generating monitoring plan...');
|
||||||
@@ -85,6 +88,8 @@ const EnhancedStrategyActivationButton: React.FC<EnhancedStrategyActivationButto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActivationProgress(60);
|
||||||
|
|
||||||
// Step 2: Activate strategy with monitoring plan
|
// Step 2: Activate strategy with monitoring plan
|
||||||
console.log('🎯 Activating strategy with monitoring...');
|
console.log('🎯 Activating strategy with monitoring...');
|
||||||
try {
|
try {
|
||||||
@@ -98,19 +103,31 @@ const EnhancedStrategyActivationButton: React.FC<EnhancedStrategyActivationButto
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not activate strategy with monitoring:', error);
|
console.warn('Could not activate strategy with monitoring:', error);
|
||||||
// Continue with local activation only
|
// Continue with local activation only
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActivationProgress(80);
|
||||||
|
|
||||||
// Step 3: Call the local confirmation function
|
// Step 3: Call the local confirmation function
|
||||||
console.log('🎯 EnhancedStrategyActivationButton: Calling onConfirmStrategy()');
|
console.log('🎯 EnhancedStrategyActivationButton: Calling onConfirmStrategy()');
|
||||||
await onConfirmStrategy();
|
await onConfirmStrategy();
|
||||||
console.log('🎯 EnhancedStrategyActivationButton: onConfirmStrategy() completed');
|
console.log('🎯 EnhancedStrategyActivationButton: onConfirmStrategy() completed');
|
||||||
// } catch (error) {
|
|
||||||
// setIsLoading(false);
|
setActivationProgress(100);
|
||||||
// console.error('Strategy activation failed:', error);
|
setIsSuccess(true);
|
||||||
// throw error;
|
setShowSuccessMessage(true);
|
||||||
// }
|
|
||||||
|
// Reset success state after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsSuccess(false);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Strategy activation failed:', error);
|
||||||
|
// Optional: Show error message
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setActivationProgress(0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* const setupAnalyticsAndMonitoring = async (strategyId: number, monitoringPlan: any) => {
|
/* const setupAnalyticsAndMonitoring = async (strategyId: number, monitoringPlan: any) => {
|
||||||
@@ -212,6 +229,8 @@ const EnhancedStrategyActivationButton: React.FC<EnhancedStrategyActivationButto
|
|||||||
size={20}
|
size={20}
|
||||||
sx={{ color: 'white' }}
|
sx={{ color: 'white' }}
|
||||||
/>
|
/>
|
||||||
|
) : isSuccess ? (
|
||||||
|
<CheckIcon />
|
||||||
) : strategyConfirmed ? (
|
) : strategyConfirmed ? (
|
||||||
<AutoAwesomeIcon />
|
<AutoAwesomeIcon />
|
||||||
) : (
|
) : (
|
||||||
@@ -289,13 +308,24 @@ const EnhancedStrategyActivationButton: React.FC<EnhancedStrategyActivationButto
|
|||||||
) : isSuccess ? (
|
) : isSuccess ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="success"
|
key="success"
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
variants={successVariants}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
initial="initial"
|
||||||
transition={{ duration: 0.3 }}
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
>
|
>
|
||||||
<Typography variant="button" sx={{ fontWeight: 600 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
Strategy Activated! 🎉
|
<CelebrationIcon />
|
||||||
</Typography>
|
<Typography variant="button" sx={{ fontWeight: 600 }}>
|
||||||
|
Strategy Activated!
|
||||||
|
</Typography>
|
||||||
|
<motion.span
|
||||||
|
variants={confettiVariants}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
>
|
||||||
|
🎉
|
||||||
|
</motion.span>
|
||||||
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : strategyConfirmed ? (
|
) : strategyConfirmed ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -85,17 +85,37 @@ export const useAgentHuddleFeed = (options?: { detailTier?: 'summary' | 'detaile
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stopRef.current = false;
|
stopRef.current = false;
|
||||||
let pollingTimer: ReturnType<typeof setInterval> | null = null;
|
let pollingTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let backoffMs = BASE_BACKOFF_MS;
|
||||||
|
|
||||||
|
// If polling fails repeatedly, try to reconnect SSE
|
||||||
|
const handlePollingError = () => {
|
||||||
|
if (connectionMode === 'polling' && reconnectAttemptRef.current < 5) {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
setConnectionMode('polling');
|
setConnectionMode('polling');
|
||||||
if (pollingTimer) clearInterval(pollingTimer);
|
if (pollingTimer) clearInterval(pollingTimer);
|
||||||
pollingTimer = setInterval(async () => {
|
|
||||||
|
const poll = async () => {
|
||||||
|
if (document.hidden) return;
|
||||||
try {
|
try {
|
||||||
await loadSnapshot(cursorRef.current);
|
await loadSnapshot(cursorRef.current);
|
||||||
} catch {
|
backoffMs = BASE_BACKOFF_MS;
|
||||||
// no-op
|
} catch (err: any) {
|
||||||
|
if (err?.response?.status === 429) {
|
||||||
|
// Exponential backoff for 429s
|
||||||
|
backoffMs = Math.min(backoffMs * 2, 60000);
|
||||||
|
clearInterval(pollingTimer!);
|
||||||
|
pollingTimer = setInterval(poll, backoffMs);
|
||||||
|
} else {
|
||||||
|
handlePollingError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 7000);
|
};
|
||||||
|
|
||||||
|
pollingTimer = setInterval(poll, 10000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user