diff --git a/.github/workflows/deploy-to-hf.yml b/.github/workflows/deploy-to-hf.yml index 602913a3..c2982aa1 100644 --- a/.github/workflows/deploy-to-hf.yml +++ b/.github/workflows/deploy-to-hf.yml @@ -55,4 +55,4 @@ jobs: cd hf-space git add . git commit -m "${{ github.event.head_commit.message || 'Update from GitHub Actions' }}" - git push -f origin main \ No newline at end of file + git push -f origin main diff --git a/auto-analyst-backend/Dockerfile b/auto-analyst-backend/Dockerfile index 480e44ca..0f77d7c1 100644 --- a/auto-analyst-backend/Dockerfile +++ b/auto-analyst-backend/Dockerfile @@ -10,4 +10,23 @@ COPY --chown=user ./requirements.txt requirements.txt RUN pip install --no-cache-dir --upgrade -r requirements.txt COPY --chown=user . /app -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file + +# Verify agents_config.json was copied (it should be in the backend directory) +RUN if [ -f "/app/agents_config.json" ]; then \ + echo "✅ agents_config.json found in container"; \ + ls -la /app/agents_config.json; \ + else \ + echo "⚠️ agents_config.json not found in container - will use fallback templates"; \ + ls -la /app/ | grep -E "agents|config" || echo "No config files found"; \ + fi + +# Make entrypoint script executable +USER root +RUN chmod +x /app/entrypoint_local.sh +# Make populate script executable +RUN chmod +x /app/scripts/populate_agent_templates.py + +USER user + +# Use the entrypoint script instead of directly running uvicorn +CMD ["/app/entrypoint_local.sh"] \ No newline at end of file diff --git a/auto-analyst-backend/app.py b/auto-analyst-backend/app.py index 23218f18..a781599e 100644 --- a/auto-analyst-backend/app.py +++ b/auto-analyst-backend/app.py @@ -150,10 +150,10 @@ class DeepAnalysisResponse(BaseModel): # Add near the top of the file, after imports DEFAULT_MODEL_CONFIG = { "provider": os.getenv("MODEL_PROVIDER", "openai"), - "model": os.getenv("MODEL_NAME", "gpt-4o-mini"), + "model": os.getenv("MODEL_NAME", "01"), "api_key": os.getenv("OPENAI_API_KEY"), "temperature": float(os.getenv("TEMPERATURE", 1.0)), - "max_tokens": int(os.getenv("MAX_TOKENS", 6000)) + "max_tokens": int(os.getenv("MAX_TOKENS", 6000)), "cache": False } # Create default LM config but don't set it globally @@ -162,29 +162,39 @@ class DeepAnalysisResponse(BaseModel): model=f"groq/{DEFAULT_MODEL_CONFIG["model"]}", api_key=DEFAULT_MODEL_CONFIG["api_key"], temperature=DEFAULT_MODEL_CONFIG["temperature"], - max_tokens=DEFAULT_MODEL_CONFIG["max_tokens"] + max_tokens=DEFAULT_MODEL_CONFIG["max_tokens"], cache=False ) + elif DEFAULT_MODEL_CONFIG["provider"].lower() == "gemini": default_lm = dspy.LM( model=f"gemini/{DEFAULT_MODEL_CONFIG['model']}", api_key=DEFAULT_MODEL_CONFIG["api_key"], temperature=DEFAULT_MODEL_CONFIG["temperature"], - max_tokens=DEFAULT_MODEL_CONFIG["max_tokens"] + max_tokens=DEFAULT_MODEL_CONFIG["max_tokens"], cache=False ) elif DEFAULT_MODEL_CONFIG["provider"].lower() == "anthropic": default_lm = dspy.LM( model=f"anthropic/{DEFAULT_MODEL_CONFIG["model"]}", api_key=DEFAULT_MODEL_CONFIG["api_key"], temperature=DEFAULT_MODEL_CONFIG["temperature"], - max_tokens=DEFAULT_MODEL_CONFIG["max_tokens"] + max_tokens=DEFAULT_MODEL_CONFIG["max_tokens"], cache=False ) else: - default_lm = dspy.LM( - model=f"openai/{DEFAULT_MODEL_CONFIG["model"]}", + if DEFAULT_MODEL_CONFIG['model'].lower() in ['openai/gpt-5', 'openai/gpt-5-mini','openai/gpt-5-nano']: + default_lm = dspy.LM( + model=f"gemini/{DEFAULT_MODEL_CONFIG['model']}", api_key=DEFAULT_MODEL_CONFIG["api_key"], temperature=DEFAULT_MODEL_CONFIG["temperature"], - max_tokens=DEFAULT_MODEL_CONFIG["max_tokens"] - ) + max_tokens=None, + max_completion_tokens= DEFAULT_MODEL_CONFIG["max_tokens"], cache=False + ) + else: + default_lm = dspy.LM( + model=f"openai/{DEFAULT_MODEL_CONFIG["model"]}", + api_key=DEFAULT_MODEL_CONFIG["api_key"], + temperature=DEFAULT_MODEL_CONFIG["temperature"], + max_tokens=DEFAULT_MODEL_CONFIG["max_tokens"], cache=False + ) # lm = dspy.LM('openai/gpt-4o-mini', api_key=os.getenv("OPENAI_API_KEY")) # dspy.configure(lm=lm) @@ -204,7 +214,7 @@ def get_session_lm(session_state): model=f"groq/{model_config.get("model", DEFAULT_MODEL_CONFIG["model"])}", api_key=model_config.get("api_key", DEFAULT_MODEL_CONFIG["api_key"]), temperature=model_config.get("temperature", DEFAULT_MODEL_CONFIG["temperature"]), - max_tokens=model_config.get("max_tokens", DEFAULT_MODEL_CONFIG["max_tokens"]) + max_tokens=model_config.get("max_tokens", DEFAULT_MODEL_CONFIG["max_tokens"]), cache=False ) elif provider == "anthropic": logger.log_message(f"Using anthropic model: {model_config.get('model', DEFAULT_MODEL_CONFIG['model'])}", level=logging.INFO) @@ -212,7 +222,7 @@ def get_session_lm(session_state): model=f"anthropic/{model_config.get("model", DEFAULT_MODEL_CONFIG["model"])}", api_key=model_config.get("api_key", DEFAULT_MODEL_CONFIG["api_key"]), temperature=model_config.get("temperature", DEFAULT_MODEL_CONFIG["temperature"]), - max_tokens=model_config.get("max_tokens", DEFAULT_MODEL_CONFIG["max_tokens"]) + max_tokens=model_config.get("max_tokens", DEFAULT_MODEL_CONFIG["max_tokens"]), cache=False ) elif provider == "gemini": logger.log_message(f"Using gemini model: {model_config.get('model', DEFAULT_MODEL_CONFIG['model'])}", level=logging.INFO) @@ -220,16 +230,34 @@ def get_session_lm(session_state): model=f"gemini/{model_config.get('model', DEFAULT_MODEL_CONFIG['model'])}", api_key=model_config.get("api_key", DEFAULT_MODEL_CONFIG["api_key"]), temperature=model_config.get("temperature", DEFAULT_MODEL_CONFIG["temperature"]), - max_tokens=model_config.get("max_tokens", DEFAULT_MODEL_CONFIG["max_tokens"]) + max_tokens=model_config.get("max_tokens", DEFAULT_MODEL_CONFIG["max_tokens"]), cache=False ) else: # OpenAI is the default - logger.log_message(f"Using default model: {model_config.get('model', DEFAULT_MODEL_CONFIG['model'])}", level=logging.INFO) - return dspy.LM( - model=f"openai/{model_config.get("model", DEFAULT_MODEL_CONFIG["model"])}", - api_key=model_config.get("api_key", DEFAULT_MODEL_CONFIG["api_key"]), - temperature=model_config.get("temperature", DEFAULT_MODEL_CONFIG["temperature"]), - max_tokens=model_config.get("max_tokens", DEFAULT_MODEL_CONFIG["max_tokens"]) - ) + model_name = model_config.get("model", DEFAULT_MODEL_CONFIG["model"]) + max_token_value = model_config.get("max_tokens", DEFAULT_MODEL_CONFIG["max_tokens"]) + + logger.log_message(f"Using default model: {model_name} with max tokens value: {max_token_value}", level=logging.INFO) + + if 'gpt-5' in model_name: + # For gpt-5 model, use max_completion_token (singular) argument name, + # but its value is what max_tokens used to be + return dspy.LM( + model=f"openai/{model_name}", + api_key=model_config.get("api_key", DEFAULT_MODEL_CONFIG["api_key"]), + temperature=model_config.get("temperature", DEFAULT_MODEL_CONFIG["temperature"]), + max_tokens=None, + max_completion_tokens=model_config.get("max_tokens", 3000), cache=False + ) + else: + # For other models, keep using max_tokens as parameter name + return dspy.LM( + model=f"openai/{model_name}", + api_key=model_config.get("api_key", DEFAULT_MODEL_CONFIG["api_key"]), + temperature=model_config.get("temperature", DEFAULT_MODEL_CONFIG["temperature"]), + max_tokens=max_token_value, cache=False + ) + + # If no valid session config, use default return default_lm @@ -454,7 +482,7 @@ async def verify_origin_middleware(request: Request, call_next): RESPONSE_ERROR_INVALID_QUERY = "Please provide a valid query..." RESPONSE_ERROR_NO_DATASET = "No dataset is currently loaded. Please link a dataset before proceeding with your analysis." DEFAULT_TOKEN_RATIO = 1.5 -REQUEST_TIMEOUT_SECONDS = 120 # Timeout for LLM requests +REQUEST_TIMEOUT_SECONDS = 30 # Timeout for LLM requests MAX_RECENT_MESSAGES = 3 DB_BATCH_SIZE = 10 # For future batch DB operations @@ -895,115 +923,159 @@ async def _generate_streaming_responses(session_state: dict, query: str, session # Add chat context from previous messages enhanced_query = _prepare_query_with_context(query, session_state) - # Use the session model for this specific request - with dspy.context(lm=session_lm): - try: - # Get the plan - planner is now async, so we need to await it - plan_response = await session_state["ai_system"].get_plan(enhanced_query) - - plan_description = format_response_to_markdown( - {"analytical_planner": plan_response}, - dataframe=session_state["current_df"] - ) - - # Check if plan is valid - if plan_description == RESPONSE_ERROR_INVALID_QUERY: - yield json.dumps({ - "agent": "Analytical Planner", - "content": plan_description, - "status": "error" - }) + "\n" - return - + try: + # Get the plan - planner is now async, so we need to await it + plan_response = await session_state["ai_system"].get_plan(enhanced_query) + + plan_description = format_response_to_markdown( + {"analytical_planner": plan_response}, + dataframe=session_state["current_df"] + ) + + # Check if plan is valid + if plan_description == RESPONSE_ERROR_INVALID_QUERY: yield json.dumps({ "agent": "Analytical Planner", "content": plan_description, - "status": "success" if plan_description else "error" + "status": "error" }) + "\n" + return + + yield json.dumps({ + "agent": "Analytical Planner", + "content": plan_description, + "status": "success" if plan_description else "error" + }) + "\n" + + # Track planner usage + if session_state.get("user_id"): + planner_tokens = _estimate_tokens(ai_manager=app.state.ai_manager, + input_text=enhanced_query, + output_text=plan_description) - # Track planner usage - if session_state.get("user_id"): - planner_tokens = _estimate_tokens(ai_manager=app.state.ai_manager, - input_text=enhanced_query, - output_text=plan_description) - - usage_records.append(_create_usage_record( - session_state=session_state, - model_name=session_state.get("model_config", DEFAULT_MODEL_CONFIG)["model"], - prompt_tokens=planner_tokens["prompt"], - completion_tokens=planner_tokens["completion"], - query_size=len(enhanced_query), - response_size=len(plan_description), - processing_time_ms=int((time.time() - overall_start_time) * 1000), - is_streaming=False - )) - - # Execute the plan with well-managed concurrency - async for agent_name, inputs, response in _execute_plan_with_timeout( - session_state["ai_system"], enhanced_query, plan_response): - - if agent_name == "plan_not_found": - yield json.dumps({ - "agent": "Analytical Planner", - "content": "**No plan found**\n\nPlease try again with a different query or try using a different model.", - "status": "error" - }) + "\n" - return + usage_records.append(_create_usage_record( + session_state=session_state, + model_name=session_state.get("model_config", DEFAULT_MODEL_CONFIG)["model"], + prompt_tokens=planner_tokens["prompt"], + completion_tokens=planner_tokens["completion"], + query_size=len(enhanced_query), + response_size=len(plan_description), + processing_time_ms=int((time.time() - overall_start_time) * 1000), + is_streaming=False + )) + + logger.log_message(f"Plan response: {plan_response}", level=logging.INFO) + logger.log_message(f"Plan response type: {type(plan_response)}", level=logging.INFO) + + # Check if plan_response is valid + # if not plan_response or not isinstance(plan_response, dict): + # yield json.dumps({ + # "agent": "Analytical Planner", + # "content": "**Error: Invalid plan response**\n\nResponse: " + str(plan_response), + # "status": "error" + # }) + "\n" + # return + + # Execute the plan with well-managed concurrency + with dspy.context(lm = session_lm): + try: - formatted_response = format_response_to_markdown( - {agent_name: response}, - dataframe=session_state["current_df"] - ) or "No response generated" + async for agent_name, inputs, response in session_state["ai_system"].execute_plan(enhanced_query, plan_response): + + if agent_name == "plan_not_found": + yield json.dumps({ + "agent": "Analytical Planner", + "content": "**No plan found**\n\nPlease try again with a different query or try using a different model.", + "status": "error" + }) + "\n" + return + + if agent_name == "plan_not_formated_correctly": + yield json.dumps({ + "agent": "Analytical Planner", + "content": "**Something went wrong with formatting, retry the query!**", + "status": "error" + }) + "\n" + return + + + formatted_response = format_response_to_markdown( + {agent_name: response}, + dataframe=session_state["current_df"] + ) - if formatted_response == RESPONSE_ERROR_INVALID_QUERY: yield json.dumps({ - "agent": agent_name, + "agent": agent_name.split("__")[0] if "__" in agent_name else agent_name, "content": formatted_response, - "status": "error" + "status": "success" if response else "error" }) + "\n" - return - # Send response chunk + # Handle agent errors + if isinstance(response, dict) and "error" in response: + yield json.dumps({ + "agent": agent_name, + "content": f"**Error in {agent_name}**: {response['error']}", + "status": "error" + }) + "\n" + continue # Continue with next agent instead of returning + + + + if formatted_response == RESPONSE_ERROR_INVALID_QUERY: + yield json.dumps({ + "agent": agent_name, + "content": formatted_response, + "status": "error" + }) + "\n" + continue # Continue with next agent instead of returning + + # Send response chunk + + + # Track agent usage for future batch DB write + if session_state.get("user_id"): + agent_tokens = _estimate_tokens( + ai_manager=app.state.ai_manager, + input_text=str(inputs), + output_text=str(response) + ) + + # Get appropriate model name for code combiner + if "code_combiner_agent" in agent_name and "__" in agent_name: + provider = agent_name.split("__")[1] + model_name = _get_model_name_for_provider(provider) + else: + model_name = session_state.get("model_config", DEFAULT_MODEL_CONFIG)["model"] + + usage_records.append(_create_usage_record( + session_state=session_state, + model_name=model_name, + prompt_tokens=agent_tokens["prompt"], + completion_tokens=agent_tokens["completion"], + query_size=len(str(inputs)), + response_size=len(str(response)), + processing_time_ms=int((time.time() - overall_start_time) * 1000), + is_streaming=True + )) + + except asyncio.TimeoutError: yield json.dumps({ - "agent": agent_name.split("__")[0] if "__" in agent_name else agent_name, - "content": formatted_response, - "status": "success" if response else "error" + "agent": "planner", + "content": "The request timed out. Please try a simpler query.", + "status": "error" }) + "\n" + return - # Track agent usage for future batch DB write - if session_state.get("user_id"): - agent_tokens = _estimate_tokens( - ai_manager=app.state.ai_manager, - input_text=str(inputs), - output_text=str(response) - ) - - # Get appropriate model name for code combiner - if "code_combiner_agent" in agent_name and "__" in agent_name: - provider = agent_name.split("__")[1] - model_name = _get_model_name_for_provider(provider) - else: - model_name = session_state.get("model_config", DEFAULT_MODEL_CONFIG)["model"] - - usage_records.append(_create_usage_record( - session_state=session_state, - model_name=model_name, - prompt_tokens=agent_tokens["prompt"], - completion_tokens=agent_tokens["completion"], - query_size=len(str(inputs)), - response_size=len(str(response)), - processing_time_ms=int((time.time() - overall_start_time) * 1000), - is_streaming=True - )) - - except asyncio.TimeoutError: - yield json.dumps({ - "agent": "planner", - "content": "The request timed out. Please try a simpler query.", - "status": "error" - }) + "\n" - return - except Exception as e: + except Exception as e: + logger.log_message(f"Error executing plan: {str(e)}", level=logging.ERROR) + yield json.dumps({ + "agent": "planner", + "content": f"An error occurred while executing the plan: {str(e)}", + "status": "error" + }) + "\n" + return + + except Exception as e: logger.log_message(f"Error in streaming response: {str(e)}", level=logging.ERROR) yield json.dumps({ "agent": "planner", @@ -1066,18 +1138,6 @@ def _get_model_name_for_provider(provider: str) -> str: return provider_model_map.get(provider, "o3-mini") -async def _execute_plan_with_timeout(ai_system, enhanced_query, plan_response): - """Execute the plan with timeout handling for each step""" - try: - logger.log_message(f"Plan response: {plan_response}", level=logging.INFO) - # Use the async generator from execute_plan directly - async for agent_name, inputs, response in ai_system.execute_plan(enhanced_query, plan_response): - # Yield results as they come - yield agent_name, inputs, response - except Exception as e: - logger.log_message(f"Error executing plan: {str(e)}", level=logging.ERROR) - yield "error", None, {"error": "An error occurred during plan execution"} - # Add an endpoint to list available agents @app.get("/agents", response_model=dict) @@ -1593,4 +1653,4 @@ async def download_html_report( app.include_router(templates_router) if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/auto-analyst-backend/requirements.txt b/auto-analyst-backend/requirements.txt index 7b5fa813..f84443c8 100644 --- a/auto-analyst-backend/requirements.txt +++ b/auto-analyst-backend/requirements.txt @@ -1,16 +1,16 @@ aiofiles==24.1.0 beautifulsoup4==4.13.4 -dspy==2.6.14 +dspy==2.6.27 +litellm==1.75.2 email_validator==2.2.0 -fastapi==0.111.1 +fastapi==0.115.5 fastapi-cli==0.0.7 FastAPI-SQLAlchemy==0.2.1 -fastapi-sso==0.10.0 +fastapi-sso==0.16.0 groq==0.18.0 -gunicorn==22.0.0 +gunicorn==23.0.0 huggingface-hub==0.30.2 joblib==1.4.2 -litellm==1.63.7 llama-cloud==0.1.19 llama-cloud-services==0.6.21 llama-index==0.12.14 @@ -29,15 +29,14 @@ matplotlib-inline==0.1.7 numpy==2.2.2 openpyxl==3.1.2 xlrd==2.0.1 -openai==1.61.0 +openai==1.97.0 pandas==2.2.3 -polars==1.30.0 +polars==1.31.0 pillow==11.1.0 plotly==5.24.1 psycopg2==2.9.10 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -python-multipart==0.0.9 requests==2.32.3 scikit-learn==1.6.1 scipy==1.15.1 @@ -51,8 +50,8 @@ tiktoken==0.8.0 tokenizers==0.21.0 tqdm==4.67.1 urllib3==2.4.0 -uvicorn==0.22.0 -websockets==14.2 +uvicorn==0.29.0 +websockets>=13.1.0 wheel==0.45.1 xgboost-cpu==3.0.2 bokeh==3.7.3 @@ -60,4 +59,5 @@ pymc==5.23.0 lightgbm==4.6.0 arviz==0.21.0 optuna==4.3.0 -shap==0.45.1 \ No newline at end of file +shap==0.45.1 +litellm[proxy] diff --git a/auto-analyst-backend/scripts/format_response.py b/auto-analyst-backend/scripts/format_response.py index 7f2af8e6..6bc04e7c 100644 --- a/auto-analyst-backend/scripts/format_response.py +++ b/auto-analyst-backend/scripts/format_response.py @@ -856,7 +856,7 @@ def format_plan_instructions(plan_instructions): else: raise TypeError(f"Unsupported plan instructions type: {type(plan_instructions)}") except Exception as e: - raise ValueError(f"Error processing plan instructions: {str(e)}") + raise ValueError(f"Error processing plan instructions: {str(e)} + {dspy.settings.lm} ") # logger.log_message(f"Plan instructions: {instructions}", level=logging.INFO) @@ -1059,7 +1059,7 @@ def format_response_to_markdown(api_response, agent_name = None, dataframe=None) except Exception as e: logger.log_message(f"Error in format_response_to_markdown: {str(e)}", level=logging.ERROR) - return f"{str(e)}" + return f"error formating markdown {str(e)}" # logger.log_message(f"Generated markdown content for agent '{agent_name}' at {time.strftime('%Y-%m-%d %H:%M:%S')}: {markdown}, length: {len(markdown)}", level=logging.INFO) @@ -1070,7 +1070,7 @@ def format_response_to_markdown(api_response, agent_name = None, dataframe=None) f"API Response: {api_response}", level=logging.ERROR ) - return " " + return "" return '\n'.join(markdown) @@ -1100,4 +1100,4 @@ def format_response_to_markdown(api_response, agent_name = None, dataframe=None) } } - formatted_md = format_response_to_markdown(sample_response) \ No newline at end of file + formatted_md = format_response_to_markdown(sample_response) diff --git a/auto-analyst-backend/src/agents/agents.py b/auto-analyst-backend/src/agents/agents.py index dcb44c8f..fd27ffd3 100644 --- a/auto-analyst-backend/src/agents/agents.py +++ b/auto-analyst-backend/src/agents/agents.py @@ -4,6 +4,8 @@ from dotenv import load_dotenv import logging from src.utils.logger import Logger +import json + load_dotenv() logger = Logger("agents", see_time=True, console_log=False) @@ -626,9 +628,9 @@ def __init__(self): self.planners = { - "advanced":dspy.asyncify(dspy.ChainOfThought(advanced_query_planner)), - "intermediate":dspy.asyncify(dspy.ChainOfThought(intermediate_query_planner)), - "basic":dspy.asyncify(dspy.ChainOfThought(basic_query_planner)), + "advanced":dspy.asyncify(dspy.Predict(advanced_query_planner)), + "intermediate":dspy.asyncify(dspy.Predict(intermediate_query_planner)), + "basic":dspy.asyncify(dspy.Predict(basic_query_planner)), # "unrelated":dspy.Predict(self.basic_qa_agent) } self.planner_desc = { @@ -642,76 +644,89 @@ def __init__(self): self.allocator = dspy.Predict("goal,planner_desc,dataset->exact_word_complexity,reasoning") async def forward(self, goal, dataset, Agent_desc): - # Check if we have any agents available - if not Agent_desc or Agent_desc == "[]" or len(str(Agent_desc).strip()) < 10: + + if not Agent_desc or Agent_desc == "[]": logger.log_message("No agents available for planning", level=logging.WARNING) return { "complexity": "no_agents_available", "plan": "no_agents_available", "plan_instructions": {"message": "No agents are currently enabled for analysis. Please enable at least one agent (preprocessing, statistical analysis, machine learning, or visualization) in your template preferences to proceed with data analysis."} } - - try: - complexity = self.allocator(goal=goal, planner_desc=str(self.planner_desc), dataset=str(dataset)) - # If complexity is unrelated, return basic_qa_agent - if complexity.exact_word_complexity.strip() == "unrelated": - return { - "complexity": complexity.exact_word_complexity.strip(), - "plan": "basic_qa_agent", - "plan_instructions": "{'basic_qa_agent':'Not a data related query, please ask a data related-query'}" - } + lm = dspy.LM('openai/gpt-4o-mini',max_tokens=400) + with dspy.context(lm= lm): + + # Check if we have any agents available + - # Try to get plan with determined complexity try: - logger.log_message(f"Attempting to plan with complexity: {complexity.exact_word_complexity.strip()}", level=logging.DEBUG) - plan = await self.planners[complexity.exact_word_complexity.strip()](goal=goal, dataset=dataset, Agent_desc=Agent_desc) - logger.log_message(f"Plan generated successfully: {plan}", level=logging.DEBUG) - - # Check if the planner returned no_agents_available - if hasattr(plan, 'plan') and 'no_agents_available' in str(plan.plan): - logger.log_message("Planner returned no_agents_available", level=logging.WARNING) - output = { - "complexity": "no_agents_available", - "plan": "no_agents_available", - "plan_instructions": {"message": "No agents are currently enabled for analysis. Please enable at least one agent (preprocessing, statistical analysis, machine learning, or visualization) in your template preferences to proceed with data analysis."} - } - else: - output = { + + complexity = self.allocator(goal=goal, planner_desc=str(self.planner_desc), dataset=str(dataset)) + # If complexity is unrelated, return basic_qa_agent + if complexity.exact_word_complexity.strip() == "unrelated": + return { "complexity": complexity.exact_word_complexity.strip(), - "plan": dict(plan) + "plan": "basic_qa_agent", + "plan_instructions": "{'basic_qa_agent':'Not a data related query, please ask a data related-query'}" } - - except Exception as e: - logger.log_message(f"Error with {complexity.exact_word_complexity.strip()} planner, falling back to intermediate: {str(e)}", level=logging.WARNING) - # Fallback to intermediate planner - plan = await self.planners["intermediate"](goal=goal, dataset=dataset, Agent_desc=Agent_desc) - logger.log_message(f"Fallback plan generated: {plan}", level=logging.DEBUG) - # Check if the fallback planner also returned no_agents_available - if hasattr(plan, 'plan') and 'no_agents_available' in str(plan.plan): - logger.log_message("Fallback planner also returned no_agents_available", level=logging.WARNING) - output = { - "complexity": "no_agents_available", - "plan": "no_agents_available", - "plan_instructions": {"message": "No agents are currently enabled for analysis. Please enable at least one agent (preprocessing, statistical analysis, machine learning, or visualization) in your template preferences to proceed with data analysis."} - } - else: - output = { - "complexity": "intermediate", - "plan": dict(plan) - } - - except Exception as e: - logger.log_message(f"Error in planner forward: {str(e)}", level=logging.ERROR) - # Return error response - return { - "complexity": "error", - "plan": "basic_qa_agent", - "plan_instructions": {"error": f"Planning error: {str(e)}"} - } + except Exception as e: + logger.log_message(f"Error in planner forward: {str(e)}", level=logging.ERROR) + # Return error response + return { + "complexity": "error", + "plan": "basic_qa_agent", + "plan_instructions": {"error": f"Planning error in agents: {str(e)} + {dspy.settings.lm.model}"} + } + + # Try to get plan with determined complexity + # try: + logger.log_message(f"Attempting to plan with complexity: {complexity.exact_word_complexity.strip()}", level=logging.DEBUG) + with dspy.context(lm = dspy.LM('openai/gpt-4o-mini',max_tokens=3000)): + plan = await self.planners[complexity.exact_word_complexity.strip()](goal=goal, dataset=dataset, Agent_desc=Agent_desc) + logger.log_message(f"Plan generated successfully: {plan}", level=logging.DEBUG) - return output + # Check if the planner returned no_agents_available + if hasattr(plan, 'plan') and 'no_agents_available' in str(plan.plan): + logger.log_message("Planner returned no_agents_available", level=logging.WARNING) + output = { + "complexity": "no_agents_available", + "plan": "no_agents_available", + "plan_instructions": {"message": "No agents are currently enabled for analysis. Please enable at least one agent (preprocessing, statistical analysis, machine learning, or visualization) in your template preferences to proceed with data analysis."} + } + else: + output = { + "complexity": complexity.exact_word_complexity.strip(), + "plan": plan.plan, + "plan_instructions": plan.plan_instructions + } + + # except Exception as e: + # logger.log_message(f"Error with {complexity.exact_word_complexity.strip()} planner, falling back to basic: {str(e)}", level=logging.WARNING) + + # # Fallback to basic planner + # with dspy.context(lm = dspy.LM('openai/gpt-4o-mini',max_tokens=3000)): + # plan = await self.planners["basic"](goal=goal, dataset=dataset, Agent_desc=Agent_desc) + # logger.log_message(f"Fallback plan generated: {plan}", level=logging.DEBUG) + + # # Check if the fallback planner also returned no_agents_available + # if hasattr(plan, 'plan') and 'no_agents_available' in str(plan.plan): + # logger.log_message("Fallback planner also returned no_agents_available", level=logging.WARNING) + # output = { + # "complexity": "no_agents_available", + # "plan": "no_agents_available", + # "plan_instructions": {"message": "No agents are currently enabled for analysis. Please enable at least one agent (preprocessing, statistical analysis, machine learning, or visualization) in your template preferences to proceed with data analysis."} + # } + # else: + # output = { + # "complexity": "basic", + # "plan": plan.plan, + # "plan_instructions":plan.plan_instructions + # } + + + + return output @@ -1399,7 +1414,7 @@ async def execute_agent(self, specified_agent, inputs): except Exception as e: # logger.log_message(f"[EXECUTE] Error executing agent {specified_agent}: {str(e)}", level=logging.ERROR) - import traceback + # logger.log_message(f"[EXECUTE] Full traceback: {traceback.format_exc()}", level=logging.ERROR) return specified_agent.strip(), {"error": str(e)} @@ -1580,7 +1595,7 @@ def __init__(self, agents, retrievers, user_id=None, db_session=None): continue # Add template agent to agents dict - self.agents[template_name] = dspy.asyncify(dspy.ChainOfThought(signature)) + self.agents[template_name] = dspy.asyncify(dspy.Predict(signature)) # Determine if this is a visualization agent based on database category is_viz_agent = False @@ -1861,20 +1876,7 @@ async def _track_agent_usage(self, agent_name): except Exception as e: logger.log_message(f"Error in _track_agent_usage for {agent_name}: {str(e)}", level=logging.ERROR) - async def execute_agent(self, agent_name, inputs): - """Execute a single agent with given inputs""" - - try: - result = await self.agents[agent_name.strip()](**inputs) - - # Track usage for custom agents and templates - await self._track_agent_usage(agent_name.strip()) - - logger.log_message(f"Agent {agent_name} execution completed", level=logging.DEBUG) - return agent_name.strip(), dict(result) - except Exception as e: - logger.log_message(f"Error in execute_agent for {agent_name}: {str(e)}", level=logging.ERROR) - return agent_name.strip(), {"error": str(e)} + async def get_plan(self, query): """Get the analysis plan""" @@ -1885,51 +1887,51 @@ async def get_plan(self, query): dict_['Agent_desc'] = str(self.agent_desc) - try: - module_return = await self.planner( - goal=dict_['goal'], - dataset=dict_['dataset'], - Agent_desc=dict_['Agent_desc'] - ) - logger.log_message(f"Module return: {module_return}", level=logging.INFO) - - # Handle different plan formats - plan = module_return['plan'] - logger.log_message(f"Plan from module_return: {plan}, type: {type(plan)}", level=logging.INFO) - - # If plan is a string (agent name), convert to proper format - if isinstance(plan, str): - if 'complexity' in module_return: - complexity = module_return['complexity'] - else: - complexity = 'basic' - - plan_dict = { - 'plan': plan, - 'complexity': complexity - } - - # Add plan_instructions if available - if 'plan_instructions' in module_return: - plan_dict['plan_instructions'] = module_return['plan_instructions'] - else: - plan_dict['plan_instructions'] = {} + # try: + module_return = await self.planner( + goal=dict_['goal'], + dataset=dict_['dataset'], + Agent_desc=dict_['Agent_desc'] + ) + logger.log_message(f"Module return: {module_return}", level=logging.INFO) + + # Handle different plan formats + plan = module_return['plan'] + logger.log_message(f"Plan from module_return: {plan}, type: {type(plan)}", level=logging.INFO) + + # If plan is a string (agent name), convert to proper format + if isinstance(plan, str): + if 'complexity' in module_return: + complexity = module_return['complexity'] else: - # If plan is already a dict, use it directly - plan_dict = dict(plan) if not isinstance(plan, dict) else plan - if 'complexity' in module_return: - complexity = module_return['complexity'] - else: - complexity = 'basic' - plan_dict['complexity'] = complexity + complexity = 'basic' - logger.log_message(f"Final plan dict: {plan_dict}", level=logging.INFO) + plan_dict = { + 'plan': plan, + 'complexity': complexity + } + + # Add plan_instructions if available + if 'plan_instructions' in module_return: + plan_dict['plan_instructions'] = module_return['plan_instructions'] + else: + plan_dict['plan_instructions'] = {} + else: + # If plan is already a dict, use it directly + plan_dict = dict(plan) if not isinstance(plan, dict) else plan + if 'complexity' in module_return: + complexity = module_return['complexity'] + else: + complexity = 'basic' + plan_dict['complexity'] = complexity + + logger.log_message(f"Final plan dict: {plan_dict}", level=logging.INFO) - return plan_dict + return plan_dict - except Exception as e: - logger.log_message(f"Error in get_plan: {str(e)}", level=logging.ERROR) - raise + # except Exception as e: + # logger.log_message(f"Error in get_plan: {str(e)}", level=logging.ERROR) + # raise async def execute_plan(self, query, plan): """Execute the plan and yield results as they complete""" @@ -1940,7 +1942,6 @@ async def execute_plan(self, query, plan): dict_['hint'] = [] dict_['goal'] = query - import json # Clean and split the plan string into agent names plan_text = plan.get("plan", "").lower().replace("plan:", "").strip() @@ -1953,6 +1954,7 @@ async def execute_plan(self, query, plan): return plan_list = [agent.strip() for agent in plan_text.split("->") if agent.strip()] + logger.log_message(f"Plan list: {plan_list}", level=logging.INFO) # Parse the attached plan_instructions into a dict raw_instr = plan.get("plan_instructions", {}) @@ -1969,15 +1971,14 @@ async def execute_plan(self, query, plan): # Check if we have no valid agents to execute - if not plan_list or all(agent not in self.agents for agent in plan_list): - yield "plan_not_found", None, {"error": "No valid agents found in plan"} + if not plan_list: + if len(plan_text) != 0: + yield "plan_not_formatted_correctly", str(plan_text), {'error': "There was a error in the formatting"} + return - + # Execute agents in sequence for agent_name in plan_list: - if agent_name not in self.agents: - yield agent_name, {}, {"error": f"Agent '{agent_name}' not available"} - continue try: # Prepare inputs for the agent @@ -1985,18 +1986,23 @@ async def execute_plan(self, query, plan): # Add plan instructions if available for this agent if agent_name in plan_instructions: - inputs['plan_instructions'] = json.dumps(plan_instructions[agent_name]) + inputs['plan_instructions'] = plan_instructions[agent_name] else: - inputs['plan_instructions'] = "" + inputs['plan_instructions'] = str(plan_instructions).split(agent_name)[1].split('agent')[0] # logger.log_message(f"Agent inputs for {agent_name}: {inputs}", level=logging.INFO) + + + result = await self.agents[agent_name.strip()](**inputs) + # Track usage for custom agents and templates + await self._track_agent_usage(agent_name.strip()) # Execute the agent - agent_result_name, response = await self.execute_agent(agent_name, inputs) + - yield agent_result_name, inputs, response + yield agent_name, inputs, result except Exception as e: - logger.log_message(f"Error executing agent {agent_name}: {str(e)}", level=logging.ERROR) - yield agent_name, {}, {"error": f"Error executing {agent_name}: {str(e)}"} + logger.log_message(f"Error executing agent {agent_name}: {str(e)}", level=logging.ERROR) + yield agent_name, {}, {"error": f"Error executing {agent_name}: {str(e)}"} diff --git a/auto-analyst-backend/src/routes/session_routes.py b/auto-analyst-backend/src/routes/session_routes.py index 66f238c0..2847cfb2 100644 --- a/auto-analyst-backend/src/routes/session_routes.py +++ b/auto-analyst-backend/src/routes/session_routes.py @@ -213,14 +213,26 @@ async def update_model_settings( session_state = app_state.get_session_state(session_id) # Create the model config - model_config = { - "provider": settings.provider, - "model": settings.model, - "api_key": settings.api_key, - "temperature": settings.temperature, - "max_tokens": settings.max_tokens - } + if 'gpt-5' in str(settings.model): + model_config = { + "provider": settings.provider, + "model": settings.model, + "api_key": settings.api_key, + "temperature": settings.temperature, + "max_tokens":None, + "max_completion_tokens": 10000 + } + + else: + model_config = { + "provider": settings.provider, + "model": settings.model, + "api_key": settings.api_key, + "temperature": settings.temperature, + "max_tokens": settings.max_tokens + } + # Update only the session's model config session_state["model_config"] = model_config @@ -257,14 +269,24 @@ async def update_model_settings( temperature=settings.temperature, max_tokens=settings.max_tokens ) - else: # OpenAI is the default + elif settings.provider.lower() == "openai": # OpenAI is the default logger.log_message(f"OpenAI Model: {settings.model}", level=logging.INFO) - lm = dspy.LM( - model=f"openai/{settings.model}", - api_key=settings.api_key, - temperature=settings.temperature, - max_tokens=settings.max_tokens - ) + print(settings.model.lower()) + if 'gpt-5' in settings.model.lower(): + lm = dspy.LM( + model=f"openai/{settings.model}", + api_key=settings.api_key, + temperature=settings.temperature, + max_tokens = None, + max_completion_tokens= 10000 + ) + else: + lm = dspy.LM( + model=f"openai/{settings.model}", + api_key=settings.api_key, + temperature=settings.temperature, + max_tokens=settings.max_tokens + ) # Test the model configuration without setting it globally @@ -310,7 +332,7 @@ async def get_model_settings( # Use values from model_config with fallbacks to defaults return { "provider": model_config.get("provider", "openai"), - "model": model_config.get("model", "gpt-4o-mini"), + "model": model_config.get("model", "o1"), "hasCustomKey": bool(model_config.get("api_key")) or bool(os.getenv("CUSTOM_API_KEY")), "temperature": model_config.get("temperature", 0.7), "maxTokens": model_config.get("max_tokens", 6000) @@ -651,4 +673,4 @@ async def set_message_info( } except Exception as e: logger.log_message(f"Error setting message info: {str(e)}", level=logging.ERROR) - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file + raise HTTPException(status_code=500, detail=str(e)) diff --git a/auto-analyst-backend/src/utils/model_registry.py b/auto-analyst-backend/src/utils/model_registry.py index 1d8ac1f3..60af5567 100644 --- a/auto-analyst-backend/src/utils/model_registry.py +++ b/auto-analyst-backend/src/utils/model_registry.py @@ -1,8 +1,3 @@ -""" -Models registry for the Auto-Analyst application. -This file serves as the single source of truth for all model information. -""" - # Model providers PROVIDERS = { "openai": "OpenAI", @@ -14,18 +9,14 @@ # Cost per 1K tokens for different models MODEL_COSTS = { "openai": { - "gpt-4.1": {"input": 0.002, "output": 0.008}, - "gpt-4.1-mini": {"input": 0.0004, "output": 0.0016}, - "gpt-4.1-nano": {"input": 0.00010, "output": 0.0004}, - "gpt-4.5-preview": {"input": 0.075, "output": 0.15}, - "gpt-4o": {"input": 0.0025, "output": 0.01}, - "gpt-4o-mini": {"input": 0.00015, "output": 0.0006}, "o1": {"input": 0.015, "output": 0.06}, "o1-pro": {"input": 0.015, "output": 0.6}, "o1-mini": {"input": 0.00011, "output": 0.00044}, "o3": {"input": 0.002, "output": 0.008}, "o3-mini": {"input": 0.00011, "output": 0.00044}, - "gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015}, + "gpt-5": {"input": 0.00125, "output": 0.01}, # updated real cost + "gpt-5-mini": {"input": 0.00025, "output": 0.002}, # updated real cost + "gpt-5-nano": {"input": 0.00005, "output": 0.0004}, # updated real cost }, "anthropic": { "claude-3-5-haiku-latest": {"input": 0.00025, "output": 0.000125}, @@ -34,19 +25,12 @@ "claude-sonnet-4-20250514": {"input": 0.003, "output": 0.015}, "claude-3-opus-latest": {"input": 0.015, "output": 0.075}, "claude-opus-4-20250514": {"input": 0.015, "output": 0.075}, + "claude-opus-4-1": {"input": 0.015, "output": 0.075}, # approximate placeholder }, "groq": { "deepseek-r1-distill-llama-70b": {"input": 0.00075, "output": 0.00099}, - "llama-3.3-70b-versatile": {"input": 0.00059, "output": 0.00079}, - "llama3-8b-8192": {"input": 0.00005, "output": 0.00008}, - "llama3-70b-8192": {"input": 0.00059, "output": 0.00079}, - "mistral-saba-24b": {"input": 0.00079, "output": 0.00079}, - "gemma2-9b-it": {"input": 0.0002, "output": 0.0002}, - "qwen-qwq-32b": {"input": 0.00029, "output": 0.00039}, - "meta-llama/llama-4-maverick-17b-128e-instruct": {"input": 0.0002, "output": 0.0006}, - "meta-llama/llama-4-scout-17b-16e-instruct": {"input": 0.00011, "output": 0.00034}, - "deepseek-r1-distill-qwen-32b": {"input": 0.00075, "output": 0.00099}, - "llama-3.1-70b-versatile": {"input": 0.00059, "output": 0.00079}, + "gpt-oss-120B": {"input": 0.00075, "output": 0.00099}, + "gpt-oss-20B": {"input": 0.00075, "output": 0.00099} }, "gemini": { "gemini-2.5-pro-preview-03-25": {"input": 0.00015, "output": 0.001} @@ -60,42 +44,30 @@ "credits": 1, "models": [ "claude-3-5-haiku-latest", - "llama3-8b-8192", - "gemma2-9b-it", - "meta-llama/llama-4-scout-17b-16e-instruct" + "gpt-oss-20B" ] }, "tier2": { "name": "Standard", "credits": 3, "models": [ - "gpt-4.1-nano", - "gpt-4o-mini", "o1-mini", "o3-mini", - "qwen-qwq-32b", - "meta-llama/llama-4-maverick-17b-128e-instruct" + "gpt-5-nano" # Added ] }, "tier3": { "name": "Premium", "credits": 5, "models": [ - "gpt-4.1", - "gpt-4.1-mini", - "gpt-4o", "o3", - "gpt-3.5-turbo", "claude-3-7-sonnet-latest", "claude-3-5-sonnet-latest", "claude-sonnet-4-20250514", "deepseek-r1-distill-llama-70b", - "llama-3.3-70b-versatile", - "llama3-70b-8192", - "mistral-saba-24b", - "deepseek-r1-distill-qwen-32b", - "llama-3.1-70b-versatile", - "gemini-2.5-pro-preview-03-25" + "gpt-oss-120B", + "gemini-2.5-pro-preview-03-25", + "gpt-5-mini" # Added ] }, "tier4": { @@ -108,42 +80,41 @@ "claude-3-opus-latest", "claude-opus-4-20250514" ] + }, + "tier5": { # New highest tier + "name": "Ultimate", + "credits": 50, + "models": [ + "gpt-5", + "claude-opus-4-1" + ] } } # Model metadata (display name, context window, etc.) MODEL_METADATA = { # OpenAI - "gpt-4.1": {"display_name": "GPT-4.1", "context_window": 128000}, - "gpt-4.1-mini": {"display_name": "GPT-4.1 Mini", "context_window": 128000}, - "gpt-4.1-nano": {"display_name": "GPT-4.1 Nano", "context_window": 128000}, - "gpt-4o": {"display_name": "GPT-4o", "context_window": 128000}, - "gpt-4.5-preview": {"display_name": "GPT-4.5 Preview", "context_window": 128000}, - "gpt-4o-mini": {"display_name": "GPT-4o Mini", "context_window": 128000}, - "gpt-3.5-turbo": {"display_name": "GPT-3.5 Turbo", "context_window": 16385}, "o1": {"display_name": "o1", "context_window": 128000}, "o1-pro": {"display_name": "o1 Pro", "context_window": 128000}, "o1-mini": {"display_name": "o1 Mini", "context_window": 128000}, "o3": {"display_name": "o3", "context_window": 128000}, "o3-mini": {"display_name": "o3 Mini", "context_window": 128000}, + "gpt-5": {"display_name": "GPT-5", "context_window": 400000}, + "gpt-5-mini": {"display_name": "GPT-5 Mini", "context_window": 150000}, # estimated + "gpt-5-nano": {"display_name": "GPT-5 Nano", "context_window": 64000}, # estimated + # Anthropic "claude-3-opus-latest": {"display_name": "Claude 3 Opus", "context_window": 200000}, "claude-3-7-sonnet-latest": {"display_name": "Claude 3.7 Sonnet", "context_window": 200000}, "claude-3-5-sonnet-latest": {"display_name": "Claude 3.5 Sonnet", "context_window": 200000}, "claude-3-5-haiku-latest": {"display_name": "Claude 3.5 Haiku", "context_window": 200000}, - + "claude-opus-4-1": {"display_name": "Claude Opus 4.1", "context_window": 200000}, + # GROQ "deepseek-r1-distill-llama-70b": {"display_name": "DeepSeek R1 Distill Llama 70b", "context_window": 32768}, - "llama-3.3-70b-versatile": {"display_name": "Llama 3.3 70b", "context_window": 8192}, - "llama3-8b-8192": {"display_name": "Llama 3 8b", "context_window": 8192}, - "llama3-70b-8192": {"display_name": "Llama 3 70b", "context_window": 8192}, - "mistral-saba-24b": {"display_name": "Mistral Saba 24b", "context_window": 32768}, - "gemma2-9b-it": {"display_name": "Gemma 2 9b", "context_window": 8192}, - "qwen-qwq-32b": {"display_name": "Qwen QWQ 32b | Alibaba", "context_window": 32768}, - "meta-llama/llama-4-maverick-17b-128e-instruct": {"display_name": "Llama 4 Maverick 17b", "context_window": 128000}, - "meta-llama/llama-4-scout-17b-16e-instruct": {"display_name": "Llama 4 Scout 17b", "context_window": 16000}, - "llama-3.1-70b-versatile": {"display_name": "Llama 3.1 70b Versatile", "context_window": 8192}, - + "gpt-oss-120B": {"display_name": "OpenAI gpt oss 120B", "context_window": 128000}, + "gpt-oss-20B": {"display_name": "OpenAI gpt oss 20B", "context_window": 128000}, + # Gemini "gemini-2.5-pro-preview-03-25": {"display_name": "Gemini 2.5 Pro", "context_window": 1000000}, } @@ -206,4 +177,4 @@ def get_all_models_for_provider(provider): def get_models_by_tier(tier_id): """Get all models for a specific tier""" - return MODEL_TIERS.get(tier_id, {}).get("models", []) \ No newline at end of file + return MODEL_TIERS.get(tier_id, {}).get("models", []) diff --git a/auto-analyst-frontend/.env b/auto-analyst-frontend/.env new file mode 100644 index 00000000..00ada3b9 --- /dev/null +++ b/auto-analyst-frontend/.env @@ -0,0 +1,43 @@ +# Sample .env file for the frontend + +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET="Dp..." + +GOOGLE_CLIENT_ID=561649614725-fck7d0fss66eqmmjvk9p9rls2nrileob.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 + +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=... +SMTP_PASS=... +SMTP_FROM=... + +SALES_EMAIL=... + +# NEXT_PUBLIC_API_URL=http://localhost:8000 +# NEXT_PUBLIC_API_URL=https://auto-analyst-test-67f323333dd2.herokuapp.com +NEXT_PUBLIC_API_URL=https://firebird-tech-auto-analyst-test.hf.space +NEXT_PUBLIC_ANALYTICS_ADMIN_PASSWORD=... +NEXT_PUBLIC_ADMIN_EMAIL=... +NEXT_PUBLIC_FREE_TRIAL_LIMIT=0 + +ADMIN_API_KEY=admin123 + + +UPSTASH_REDIS_REST_URL=https://ideal-robin-15637.upstash.io +UPSTASH_REDIS_REST_TOKEN=AT0VAAIjcDFhNmYwZjJhNjk3ZWQ0YjkzOWI2ZWU3ODE5NDBkZDZkMnAxMA + +NEXT_PUBLIC_UPSTASH_REDIS_REST_URL=... +NEXT_PUBLIC_UPSTASH_REDIS_REST_TOKEN= + +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=... +STRIPE_SECRET_KEY=whsec_zXA1YFHXLmN3iqOUmcZmRgwIU0SVOp8A + +NEXT_PUBLIC_STRIPE_BASIC_YEARLY_PRICE_ID=... +NEXT_PUBLIC_STRIPE_STANDARD_YEARLY_PRICE_ID=... +NEXT_PUBLIC_STRIPE_PREMIUM_YEARLY_PRICE_ID=... + +NEXT_PUBLIC_STRIPE_BASIC_PRICE_ID=... +NEXT_PUBLIC_STRIPE_STANDARD_PRICE_ID=... +NEXT_PUBLIC_STRIPE_PREMIUM_PRICE_ID=... +NODE_ENV=development diff --git a/auto-analyst-frontend/app/api/checkout-sessions/route.ts b/auto-analyst-frontend/app/api/checkout-sessions/route.ts index cd569969..bc3844b1 100644 --- a/auto-analyst-frontend/app/api/checkout-sessions/route.ts +++ b/auto-analyst-frontend/app/api/checkout-sessions/route.ts @@ -139,4 +139,4 @@ async function calculateDiscountedAmount(amount: number, couponId: string): Prom console.error('Error calculating discount:', error) return amount } -} \ No newline at end of file +} diff --git a/auto-analyst-frontend/app/api/credits/route.ts b/auto-analyst-frontend/app/api/credits/route.ts index ec6cd68e..bedf1367 100644 --- a/auto-analyst-frontend/app/api/credits/route.ts +++ b/auto-analyst-frontend/app/api/credits/route.ts @@ -40,7 +40,7 @@ export async function POST(request: Request) { if (action === 'reset') { // Reset credits to the monthly allowance using centralized config - const defaultCredits = 0 // No free credits anymore + const defaultCredits = 20 // No free credits anymore await creditUtils.initializeTrialCredits(userIdentifier, 'manual-init', new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()) return NextResponse.json({ success: true, credits: defaultCredits }) } else if (action === 'deduct') { @@ -68,4 +68,4 @@ export async function POST(request: Request) { { status: 500 } ) } -} \ No newline at end of file +} diff --git a/auto-analyst-frontend/app/api/initialize-credits/route.ts b/auto-analyst-frontend/app/api/initialize-credits/route.ts index 840f3110..05190f51 100644 --- a/auto-analyst-frontend/app/api/initialize-credits/route.ts +++ b/auto-analyst-frontend/app/api/initialize-credits/route.ts @@ -10,8 +10,19 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } + const userId = token.sub; + // Allow custom amount via query param for testing + const searchParams = request.nextUrl.searchParams; + const amount = searchParams.get('amount') + ? parseInt(searchParams.get('amount') as string) + : parseInt(process.env.NEXT_PUBLIC_CREDITS_INITIAL_AMOUNT || '20'); + + // Initialize credits for the user + await creditUtils.initializeCredits(userId, amount); + + // This endpoint is for debugging/testing only // Don't automatically initialize credits anymore since we removed free plan @@ -35,4 +46,4 @@ export async function GET(request: NextRequest) { { status: 500 } ); } -} \ No newline at end of file +} diff --git a/auto-analyst-frontend/app/api/trial/start/route.ts b/auto-analyst-frontend/app/api/trial/start/route.ts index ef2095ad..5a9e8f24 100644 --- a/auto-analyst-frontend/app/api/trial/start/route.ts +++ b/auto-analyst-frontend/app/api/trial/start/route.ts @@ -188,4 +188,4 @@ export async function POST(request: NextRequest) { error: error.message || 'Failed to start trial' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/auto-analyst-frontend/app/api/user/credits/route.ts b/auto-analyst-frontend/app/api/user/credits/route.ts index b6874342..89154edc 100644 --- a/auto-analyst-frontend/app/api/user/credits/route.ts +++ b/auto-analyst-frontend/app/api/user/credits/route.ts @@ -28,7 +28,7 @@ export async function GET(request: NextRequest) { planName = subscriptionHash?.plan || 'Free Plan' } else { // Initialize default values for new users using centralized config - creditsTotal = 0 // No free credits anymore + creditsTotal = 20 // No free credits anymore creditsUsed = 0 resetDate = CreditConfig.getNextResetDate() lastUpdate = new Date().toISOString() diff --git a/auto-analyst-frontend/app/api/user/deduct-credits/route.ts b/auto-analyst-frontend/app/api/user/deduct-credits/route.ts index 20916e2d..72ebe745 100644 --- a/auto-analyst-frontend/app/api/user/deduct-credits/route.ts +++ b/auto-analyst-frontend/app/api/user/deduct-credits/route.ts @@ -19,16 +19,30 @@ export async function POST(request: NextRequest) { const creditsHash = await redis.hgetall(KEYS.USER_CREDITS(userId)) if (!creditsHash || !creditsHash.total) { + const defaultCredits = 20 + await redis.hset(KEYS.USER_CREDITS(userId), { + total: '20', + used: '0', + resetDate: CreditConfig.getNextResetDate(), + lastUpdate: new Date().toISOString() + }) + return NextResponse.json({ + success: true, + remaining: 20, + deducted: 0 + }) + } // No credits for users without subscription - require upgrade - return NextResponse.json({ - success: false, - error: 'UPGRADE_REQUIRED', - message: 'Please start your trial or upgrade your plan to continue.', - remaining: 0, - needsUpgrade: true - }, { status: 402 }) // Payment Required status code - } - + // return NextResponse.json({ + // success: false, + // error: 'UPGRADE_REQUIRED', + // message: 'Please start your trial or upgrade your plan to continue.', + // remaining: 0, + // needsUpgrade: true + // }, { status: 402 }) // Payment Required status code + // + + // Calculate new used amount const total = parseInt(creditsHash.total as string) const currentUsed = creditsHash.used ? parseInt(creditsHash.used as string) : 0 @@ -67,4 +81,4 @@ export async function POST(request: NextRequest) { console.error('Error deducting credits:', error) return NextResponse.json({ error: error.message || 'Failed to deduct credits' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/auto-analyst-frontend/app/pricing/page.tsx b/auto-analyst-frontend/app/pricing/page.tsx index 1f6a29a2..cb7db708 100644 --- a/auto-analyst-frontend/app/pricing/page.tsx +++ b/auto-analyst-frontend/app/pricing/page.tsx @@ -486,4 +486,4 @@ export default function PricingPage() { ); -} \ No newline at end of file +} diff --git a/auto-analyst-frontend/lib/credits-config.ts b/auto-analyst-frontend/lib/credits-config.ts index 1be559bc..a7b06cbb 100644 --- a/auto-analyst-frontend/lib/credits-config.ts +++ b/auto-analyst-frontend/lib/credits-config.ts @@ -49,9 +49,9 @@ export interface TrialConfig { * Trial period configuration - Change here to update across the entire app */ export const TRIAL_CONFIG: TrialConfig = { - duration: 2, - unit: 'days', - displayText: '2-Day Free Trial', + duration: 5, + unit: 'minutes', + displayText: 'Analyzing!', credits: 500 } @@ -388,4 +388,4 @@ export class CreditConfig { } return (total - used) > 0 } -} \ No newline at end of file +} diff --git a/auto-analyst-frontend/lib/model-registry.ts b/auto-analyst-frontend/lib/model-registry.ts index 6e38123e..8c19f420 100644 --- a/auto-analyst-frontend/lib/model-registry.ts +++ b/auto-analyst-frontend/lib/model-registry.ts @@ -14,18 +14,14 @@ export const PROVIDERS = { // Cost per 1K tokens for different models export const MODEL_COSTS = { openai: { - "gpt-4.1": { input: 0.002, output: 0.008 }, - "gpt-4.1-mini": { input: 0.0004, output: 0.0016 }, - "gpt-4.1-nano": { input: 0.00010, output: 0.0004 }, - "gpt-4.5-preview": { input: 0.075, output: 0.15 }, - "gpt-4o": { input: 0.0025, output: 0.01 }, - "gpt-4o-mini": { input: 0.00015, output: 0.0006 }, "o1": { input: 0.015, output: 0.06 }, "o1-pro": { input: 0.015, output: 0.6 }, "o1-mini": { input: 0.00011, output: 0.00044 }, "o3": { input: 0.002, output: 0.008 }, "o3-mini": { input: 0.00011, output: 0.00044 }, - "gpt-3.5-turbo": { input: 0.0005, output: 0.0015 } + "gpt-5": { input: 0.00125, output: 0.01 }, // real cost + "gpt-5-mini": { input: 0.00025, output: 0.002 }, // real cost + "gpt-5-nano": { input: 0.00005, output: 0.0004 } // real cost }, anthropic: { "claude-3-opus-latest": { input: 0.015, output: 0.075 }, @@ -33,20 +29,13 @@ export const MODEL_COSTS = { "claude-3-5-sonnet-latest": { input: 0.003, output: 0.015 }, "claude-3-5-haiku-latest": { input: 0.0008, output: 0.0004 }, "claude-sonnet-4-20250514": { input: 0.003, output: 0.015 }, - "claude-opus-4-20250514": { input: 0.015, output: 0.075 } + "claude-opus-4-20250514": { input: 0.015, output: 0.075 }, + "claude-opus-4-1": { input: 0.015, output: 0.075 } // approximate real cost }, groq: { - "deepseek-r1-distill-llama-70b": {"input": 0.00075, "output": 0.00099}, - "llama-3.3-70b-versatile": {"input": 0.00059, "output": 0.00079}, - "llama3-8b-8192": {"input": 0.00005, "output": 0.00008}, - "llama3-70b-8192": {"input": 0.00059, "output": 0.00079}, - "mistral-saba-24b": {"input": 0.00079, "output": 0.00079}, - "gemma2-9b-it": {"input": 0.0002, "output": 0.0002}, - "qwen-qwq-32b": {"input": 0.00029, "output": 0.00039}, - "meta-llama/llama-4-maverick-17b-128e-instruct": {"input": 0.0002, "output": 0.0006}, - "meta-llama/llama-4-scout-17b-16e-instruct": {"input": 0.00011, "output": 0.00034}, - "deepseek-r1-distill-qwen-32b": {"input": 0.00075, "output": 0.00099}, - "llama-3.1-70b-versatile": {"input": 0.00059, "output": 0.00079}, + "deepseek-r1-distill-llama-70b": { input: 0.00075, output: 0.00099 }, + "gpt-oss-120B": { input: 0.00075, output: 0.00099 }, + "gpt-oss-20B": { input: 0.00075, output: 0.00099 } }, gemini: { "gemini-2.5-pro-preview-03-25": { input: 0.00015, output: 0.001 } @@ -55,85 +44,72 @@ export const MODEL_COSTS = { // Models by tier export const MODEL_TIERS = { - "tier1": { - "name": "Basic", - "credits": 1, - "models": [ - "claude-3-5-haiku-latest", - "llama3-8b-8192", - "gemma2-9b-it", - "meta-llama/llama-4-scout-17b-16e-instruct" - ] + tier1: { + name: "Basic", + credits: 1, + models: [ + "claude-3-5-haiku-latest", + "gpt-oss-20B" + ] }, - "tier2": { - "name": "Standard", - "credits": 3, - "models": [ - "gpt-4.1-nano", - "gpt-4o-mini", - "o1-mini", - "o3-mini", - "qwen-qwq-32b", - "meta-llama/llama-4-maverick-17b-128e-instruct" - ] + tier2: { + name: "Standard", + credits: 3, + models: [ + "o1-mini", + "o3-mini", + "gpt-5-nano" // added + ] }, - "tier3": { - "name": "Premium", - "credits": 5, - "models": [ - "gpt-4.1", - "gpt-4.1-mini", - "gpt-4o", - "o3", - "gpt-3.5-turbo", - "claude-3-7-sonnet-latest", - "claude-3-5-sonnet-latest", - "claude-sonnet-4-20250514", - "deepseek-r1-distill-llama-70b", - "llama-3.3-70b-versatile", - "llama3-70b-8192", - "mistral-saba-24b", - "deepseek-r1-distill-qwen-32b", - "llama-3.1-70b-versatile", - "gemini-2.5-pro-preview-03-25" - ] + tier3: { + name: "Premium", + credits: 5, + models: [ + "o3", + "claude-3-7-sonnet-latest", + "claude-3-5-sonnet-latest", + "claude-sonnet-4-20250514", + "deepseek-r1-distill-llama-70b", + "gpt-oss-120B", + "gemini-2.5-pro-preview-03-25", + "gpt-5-mini" // added + ] }, - "tier4": { - "name": "Premium Plus", - "credits": 20, - "models": [ - "gpt-4.5-preview", - "o1", - "o1-pro", - "claude-3-opus-latest", - "claude-opus-4-20250514" - ] + tier4: { + name: "Premium Plus", + credits: 20, + models: [ + "gpt-4.5-preview", + "o1", + "o1-pro", + "claude-3-opus-latest", + "claude-opus-4-20250514", + "gpt-5", // moved here + "claude-opus-4-1" // moved here + ] } }; - // Tier colors for UI components + +// Tier colors for UI components export const TIER_COLORS = { tier1: "#10B981", // Green for Basic tier tier2: "#3B82F6", // Blue for Standard tier tier3: "#8B5CF6", // Purple for Premium tier - tier4: "#F59E0B" // Orange for Premium Plus tier + tier4: "#F59E0B" // Orange for Premium Plus tier }; // Model metadata (display name, context window, etc.) export const MODEL_METADATA: Record = { // OpenAI - "gpt-4.1": { displayName: "GPT-4.1", contextWindow: 128000 }, - "gpt-4.1-mini": { displayName: "GPT-4.1 Mini", contextWindow: 128000 }, - "gpt-4.1-nano": { displayName: "GPT-4.1 Nano", contextWindow: 128000 }, - "gpt-4o": { displayName: "GPT-4o", contextWindow: 128000 }, - "gpt-4.5-preview": { displayName: "GPT-4.5 Preview", contextWindow: 128000 }, - "gpt-4o-mini": { displayName: "GPT-4o Mini", contextWindow: 128000 }, - "gpt-3.5-turbo": { displayName: "GPT-3.5 Turbo", contextWindow: 16385 }, "o1": { displayName: "o1", contextWindow: 128000 }, "o1-pro": { displayName: "o1 Pro", contextWindow: 128000 }, - "o1-mini": { displayName: "o1-mini", contextWindow: 128000 }, + "o1-mini": { displayName: "o1 Mini", contextWindow: 128000 }, "o3": { displayName: "o3", contextWindow: 128000 }, - "o3-mini": { displayName: "o3-mini", contextWindow: 128000 }, - + "o3-mini": { displayName: "o3 Mini", contextWindow: 128000 }, + "gpt-5": { displayName: "GPT-5", contextWindow: 400000 }, + "gpt-5-mini": { displayName: "GPT-5 Mini", contextWindow: 150000 }, + "gpt-5-nano": { displayName: "GPT-5 Nano", contextWindow: 64000 }, + // Anthropic "claude-3-opus-latest": { displayName: "Claude 3 Opus", contextWindow: 200000 }, "claude-3-7-sonnet-latest": { displayName: "Claude 3.7 Sonnet", contextWindow: 200000 }, @@ -141,19 +117,13 @@ export const MODEL_METADATA: Record; return ( @@ -313,4 +283,4 @@ export function getAllModelsForProvider(provider: string): string[] { */ export function getModelsByTier(tierId: string): string[] { return MODEL_TIERS[tierId as TierId]?.models || []; -} \ No newline at end of file +} diff --git a/auto-analyst-frontend/lib/redis.ts b/auto-analyst-frontend/lib/redis.ts index c292c9b3..98058c0c 100644 --- a/auto-analyst-frontend/lib/redis.ts +++ b/auto-analyst-frontend/lib/redis.ts @@ -35,6 +35,8 @@ export const KEYS = { USER_CREDITS: (userId: string) => `user:${userId}:credits`, }; + + // Credits management utilities with consolidated hash-based storage export const creditUtils = { // Get remaining credits for a user @@ -43,7 +45,7 @@ export const creditUtils = { const creditsHash = await redis.hgetall(KEYS.USER_CREDITS(userId)) if (!creditsHash || !creditsHash.total || !creditsHash.used) { // No more free credits - users must have 0 credits if no subscription - return 0 + return 20 } const total = parseInt(creditsHash.total as string) @@ -60,6 +62,21 @@ export const creditUtils = { return 0 } }, + async initializeCredits(userId: string, credits: number = parseInt(process.env.NEXT_PUBLIC_CREDITS_INITIAL_AMOUNT || '20')): Promise { + try { + // Only use hash-based approach + await redis.hset(KEYS.USER_CREDITS(userId), { + total: credits.toString(), + used: '0', + lastUpdate: new Date().toISOString(), + resetDate: this.getNextMonthFirstDay() + }); + + logger.log(`Credits initialized successfully for ${userId}: ${credits}`); + } catch (error) { + console.error('Error initializing credits:', error); + } + }, // Initialize credits for a trial user (500 credits) async initializeTrialCredits(userId: string, paymentIntentId: string, trialEndDate: string): Promise { @@ -131,6 +148,8 @@ export const creditUtils = { } }, + + // Check if a user has enough credits async hasEnoughCredits(userId: string, amount: number): Promise { const remainingCredits = await this.getRemainingCredits(userId); @@ -570,4 +589,4 @@ export const profileUtils = { return null; } } -}; \ No newline at end of file +};